React Native Cookbook
Second Edition
Step-by-step recipes for solving common React Native development problems
Dan Ward
BIRMINGHAM - MUMBAI
React Native Cookbook Second
Edition
Copyright © 2019 Packt Publishing
All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means,
without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews.
Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the
information contained in this book is sold without warranty, either express or implied. Neither the author, nor Packt Publishing or its
dealers and distributors, will be held liable for any damages caused or alleged to have been caused directly or indirectly by this book.
Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by
the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information.
Commissioning Editor: Amarabhab Banerjee
Acquisition Editor: Trusha Shriyan
Content Development Editor: Arun Nadar
Technical Editor: Leena Patil
Copy Editor: Safis Editing
Project Coordinator: Kinjal Bari
Proofreader: Safis Editing
Indexer: Tejal Daruwale Soni
Graphics: Alishon Mendonsa
Production Coordinator: Arvindkumar Gupta
First published: December 2016
Second edition: January 2019
Production reference: 1310119
Published by Packt Publishing Ltd.
Livery Place
35 Livery Street
Birmingham
B3 2PB, UK.
ISBN 978-1-78899-192-6
www.packtpub.com
mapt.io
Mapt is an online digital library that gives you full access to over 5,000 books
and videos, as well as industry leading tools to help you plan your personal
development and advance your career. For more information, please visit our
website.
Why subscribe?
Spend less time learning and more time coding with practical eBooks and
Videos from over 4,000 industry professionals
Improve your learning with Skill Plans built especially for you
Get a free eBook or video every month
Mapt is fully searchable
Copy and paste, print, and bookmark content
Packt.com
Did you know that Packt offers eBook versions of every book published, with
PDF and ePub files available? You can upgrade to the eBook version at www.packt.
com and as a print book customer, you are entitled to a discount on the eBook
copy. Get in touch with us at customercare@packtpub.com for more details.
At www.packt.com, you can also read a collection of free technical articles, sign up
for a range of free newsletters, and receive exclusive discounts and offers on
Packt books and eBooks.
Contributors
About the author
Dan Ward is a full-stack developer and web technology consultant who has a
number of years of experience working on mobile applications with React
Native, and developing web applications with React, Vue, and Angular. He's also
a co-founder at gitconnected, and co-editor at the associated Medium
publication. His professional interests include React Native development,
modern web development, and technical writing. He also has a BA in English
Literature from Florida State University.
About the reviewer
Ashok Kumar S has been working in the mobile development domain for about
six years. In his early days, he was a JavaScript and Node.js developer. Thanks
to his strong web development skills, he mastered web and mobile development.
He is a Google-certified engineer, a speaker at global-scale conferences,
including DroidCon Berlin and MODS, and also runs a YouTube channel called
AndroidABCD for Android developers. He also contributes to open source
heavily with a view to improving his e-karma. He has written books on Wear OS
programming and Mastering Firebase Toolchain. He has also reviewed books on
mobile and web development, namely, Mastering JUnit5, Android Programming
for Beginners, and Building Enterprise JavaScript Applications.
I would like to thank my family, mostly my mother, for her infinite support in every possible way, as well as
family members Shylaja, Sumitra, Krishna, and Vinisha, and my fiancee, Geetha Shree.
Packt is searching for authors like
you
If you're interested in becoming an author for Packt, please visit authors.packtpub.c
om and apply today. We have worked with thousands of developers and tech
professionals, just like you, to help them share their insight with the global tech
community. You can make a general application, apply for a specific hot topic
that we are recruiting an author for, or submit your own idea.
Table of Contents
Title Page
Copyright and Credits
React Native Cookbook Second Edition
About Packt
Why subscribe?
Packt.com
Contributors
About the author
About the reviewer
Packt is searching for authors like you
Preface
Who this book is for
What this book covers
To get the most out of this book
Download the example code files
Download the color images
Conventions used
Sections
Getting ready
How to do it…
How it works…
There's more…
See also
Get in touch
Reviews
1. Setting Up Your Environment
Technical requirements
Installing dependencies
Installing Xcode
Installing Genymotion
Installing Node.js
Installing Expo
Installing Watchman
Initializing your first app
Running your app in a simulator/emulator
Running your app on an iOS simulator
Running your app on an Android emulator
Running your app on a real device
Running your app on an iPhone or Android
Summary
Further reading
2. Creating a Simple React Native App
Adding styles to elements
Getting ready
How to do it...
How it works...
There's more...
Using images to mimic a video player
Getting ready
How to do it...
How it works...
Creating a toggle button
Getting ready
How to do it...
How it works...
There's more...
Displaying a list of items
Getting ready
How to do it...
How it works...
There's more...
Using flexbox to create a layout
Getting ready
How to do it...
How it works...
There's more...
See also
Setting up and using navigation
Getting ready
How to do it...
How it works...
See also
3. Implementing Complex User Interfaces - Part I
Creating a reusable button with theme support
Getting ready
How to do it...
How it works...
Building a complex layout for tablets using flexbox
Getting ready
How to do it...
There's more...
See also
Including custom fonts
Getting ready
How to do it...
How it works...
See also
Using font icons
Getting ready
How to do it...
How it works...
See also
4. Implementing Complex User Interfaces - Part II
Dealing with universal applications
Getting ready
How to do it...
How it works...
See also
Detecting orientation changes
Getting ready
How to do it...
There's more...
Using a WebView to embed external websites
Getting ready
How to do it...
How it works...
Linking to websites and other applications
Getting ready
How to do it...
How it works...
See also
Creating a form component
Getting ready
How to do it...
How it works...
5. Implementing Complex User Interfaces - Part III
Introduction
Creating a map app with Google Maps
Getting ready
How to do it...
How it works...
There's more...
Creating an audio player
Getting ready
How to do it...
How it works...
There's more...
Creating an image carousel
Getting ready
How to do it...
How it works...
There's more...
Adding push notifications to your app
Getting ready
How to do it...
How it works...
There's more...
Implementing browser-based authentication
Getting ready
How to do it...
How it works...
See also
6. Adding Basic Animations to Your App
Introduction
Creating simple animations
Getting ready
How to do it...
How it works...
Running multiple animations
Getting ready
How to do it...
How it works...
Creating animated notifications
Getting ready
How to do it...
How it works...
There's more...
Expanding and collapsing containers
Getting ready
How to do it...
How it works...
See also
Creating a button with a loading animation
Getting ready
How to do it...
How it works...
Conclusion
7. Adding Advanced Animations to Your App
Introduction
Removing items from a list component
Getting ready
How to do it...
How it works...
See also
Creating a Facebook reactions widget
Getting ready
How to do it...
How it works...
Displaying images in fullscreen
Getting ready
How to do it...
How it works...
See also
8. Working with Application Logic and Data
Introduction
Storing and retrieving data locally
Getting ready
How to do it...
How it works...
See also
Retrieving data from a remote API
Getting ready
How to do it...
How it works...
Sending data to a remote API
Getting ready
How to do it...
How it works...
Establishing real-time communication with WebSockets
Getting ready
How to do it...
How it works...
There's more...
Integrating persistent database functionality with Realm
Getting ready
How to do it...
How it works...
Masking the application upon network connection loss
Getting ready
How to do it...
How it works...
Synchronizing locally persisted data with a remote API
Getting ready
How to do it...
How it works...
Logging in with Facebook
Getting ready
How to do it...
How it works...
9. Implementing Redux
Introduction
Installing Redux and preparing our project
Getting started
How to do it...
How it works...
Defining actions
Getting ready
How to do it...
How it works...
There's more...
Defining reducers
Getting ready
How to do it...
How it works...
Setting up the Redux store
How to do it...
How it works...
Communicating with a remote API
Getting ready
How to do it...
How it works...
Connecting the store to the view
Getting ready
How to do it...
How it works...
Storing offline content using Redux
Getting ready
How to do it...
How it works...
10. App Workflow and Third-Party Plugins
How this chapter works
React Native development tools
Expo
React Native CLI
CocoaPods
Planning your app and choosing your workflow
How to do it...
Expo CLI setup
Using NativeBase for cross-platform UI components
Getting ready
Using a pure React Native app (React Native CLI)
Using an Expo app
How to do it... 
How it works...
Using glamorous-native for styling UI components
Getting ready
How to do it... 
How it works...
Using react-native-spinkit for adding animated loading indicators
Getting started
How to do it...
How it works...
There's more...
Using react-native-side-menu for adding side navigation menus
Getting ready
How to do it...
How it works...
Using react-native-modalbox for adding modals
Getting ready
How to do it...
How it works...
11. Adding Native Functionality - Part I
Introduction
Exposing custom iOS modules
Getting ready
How to do it...
How it works...
There's more...
See also
Rendering custom iOS view components
How to do it...
How it works...
Exposing custom Android modules
Getting ready
How to do it...
How it works...
Rendering custom Android view components
How to do it...
How it works...
Handling the Android back button
Getting ready
How to do it...
How it works...
12. Adding Native Functionality - Part II
Introduction
Reacting to changes in application state
How to do it...
How it works...
Copying and pasting content
Getting ready
How to do it...
How it works...
Receiving push notifications
Getting ready
How to do it...
How it works...
Authenticating via touch ID or fingerprint sensor
Getting ready
How to do it...
How it works...
Hiding application content when multitasking
Getting ready
How to do it...
How it works...
Background processing on iOS
Getting ready
How to do it...
How it works...
Background processing on Android
Getting ready
How to do it...
How it works...
Playing audio files on iOS
Getting ready
How to do it...
How it works...
Playing audio files on Android
Getting ready
How to do it...
How it works...
13. Integration with Native Applications
Introduction
Combining a React Native app and a Native iOS app
Getting ready
How to do it...
How it works...
See also
Communicating from an iOS app to React Native
Getting ready
How to do it...
Communicating from React Native to an iOS app container
Getting ready
How to do it...
How it works...
Handling being invoked by an external iOS app
Getting ready
How to do it...
How it works...
Embedding a React Native app inside a Native Android app
Getting ready
How to do it...
How it works...
Communicating from an Android app to React Native
Getting ready
How to do it...
How it works...
Communicating from React Native to an Android app container
Getting ready
How to do it...
How it works...
Handling being invoked by an external Android app
How to do it...
How it works...
14. Deploying Your App
Introduction
Deploying development builds to an iOS device
Getting ready
How to do it...
How it works...
Deploying development builds to an Android device
Getting ready
How to do it...
There's more...
How it works...
Deploying test builds to HockeyApp
Getting ready
How to do it...
How it works...
Deploying iOS test builds to TestFlight
Getting ready
How to do it...
How it works...
Deploying production builds to the Apple App Store
Getting ready
How to do it...
How it works...
Deploying production builds to Google Play Store
Getting ready
How to do it...
How it works...
Deploying Over-The-Air updates
Getting ready
How to do it...
How it works...
Optimizing React Native app size
Getting ready
How to do it...
How it works...
15. Optimizing the Performance of Your App
Introduction
Optimizing our JavaScript code
Getting ready
How to do it...
How it works...
Optimizing the performance of custom UI components
Getting ready
How to do it...
How it works...
See also
Keeping animations running at 60 FPS
Getting ready
How to do it...
How it works
There's more...
Getting the most out of ListView
Getting ready
How to do it...
How it works...
See also
Boosting the performance of our app
How to do it...
How it works...
Optimizing the performance of native iOS modules
Getting ready
How to do it...
How it works...
Optimizing the performance of native Android modules
Getting ready
How to do it...
How it works...
Optimizing the performance of native iOS UI components
Getting ready
How to do it...
How it works...
Optimizing the performance of native Android UI components
Getting ready
How to do it...
How it works...
Other Books You May Enjoy
Leave a review - let other readers know what you think
Preface
There are many ways for a developer to build an app for iOS or Android. React
Native stands out as one of the most stable, performant, and developer-friendly
options for building hybrid mobile apps. Developing mobile apps with React
Native allows developers to build iOS and Android apps in a single code base,
with the added ability for code-sharing between the two platforms.
Even better, a developer with experience of building web apps in React will be
ahead of the game, since many of the same patterns and conventions are carried
over into React Native. If you've had experience of building web apps with
React, or another framework based on Model, View, Component (MVC), you'll
feel right at home building mobile apps in React Native.
This book is intended to serve as a go-to reference for solutions to common
problems you'll likely face when building a wide variety of apps. Each chapter is
presented as a series of step-by-step recipes that each explain how to build a
single feature of an overall app.
React Native is an evolving language. At the time of writing, it's still in the 0.5x
stage of the development life cycle, so there are some things that will change in
the months and years to come. Best practices could morph into stale ideas, or the
open source packages highlighted here could fall out of favor. I've done all I
could to keep this text as up to date as possible, but technology moves fast, so it's
impossible for a book to keep up by itself. The repository for of all the code
covered in this book is hosted on GitHub at . If you find anything in the code
here that doesn't seem to be working correctly, you can submit an issue. Or, if
you've got a better way to do something, consider submitting a pull request!
I hope you find this book helpful on your way through the land of React Native.
Happy developing!
Who this book is for
This book has been designed with beginner React Native developers in mind.
Even if you don't have a lot of experience with web development, the JavaScript
found in this book should hopefully never be over your head. I've tried to avoid
complexity wherever possible, to keep the focus on the lesson being taught
within a given recipe.
This book also assumes the developer works on a computer running macOS.
While it is technically possible to develop React Native apps using Windows or
Linux, there are a number of limitations that make macOS machines much more
preferable for React Native development, including the abilities to work with
native iOS code via Xcode, run iOS code on the iOS simulator, and work with
the most robust development tools for React Native app development.
What this book covers
Chapter 1, Setting Up Your Environment, covers the different software we'll be
installing to get started on the development of React Native apps.
Chapter 2, Creating a Simple React Native App, covers the basics of building
layouts and navigation. The recipes in the chapter serve as an introduction to
React Native development, and cover the basic functionality found in most any
mobile app.
Chapter 3, Implementing Complex User Interfaces – Part I, covers features
including custom fonts and custom reusable themes.
Chapter 4, Implementing Complex User Interfaces – Part II, continues with more
recipes based on UI features. It covers features such as handling screen
orientation changes and building user forms.
Chapter 5, Implementing Complex User Interfaces – Part III, covers other
common features you'll likely need when building complex UIs. This chapter
covers adding map support, implementing browser-based authentication, and
creating an audio player.
Chapter 6, Adding Basic Animations to Your App, covers the basics of creating
animations.
Chapter 7, Adding Advanced Animations to Your App, continues building on the
previous chapter, with more advanced features.
Chapter 8, Working with Application Logic and Data, introduces us to building
apps that handle data. We'll cover topics including storing data locally and
handling network loss gracefully.
Chapter 9, Implementing Redux, covers implementing the Flux data patter using
the Redux library. Redux is a battle-tested way to handle data flow in React
apps, and works just as well in React Native.
Chapter 10, App Workflow and Third-Party Plugins, covers the different methods a
developer can use to build an app, along with how to build apps using open
source code.
Chapter 11, Adding Native Functionalities – Part I, covers the basics of working
with native iOS and Android code in a React Native app.
Chapter 12, Adding Native Functionalities – Part II, covers more complex
techniques for communicating between the React Native and native layers.
Chapter 13, Integration with Native Applications, covers integrating React Native
with an existing native app. Not every app can be built from scratch. These
recipes should be helpful for developers who need to integrate their work with
an app already in the App Store.
Chapter 14, Deploying Your App, covers the basic process of deploying a React
Native app, as well as details for using HockeyApp to track the metrics of your
app.
Chapter 15, Optimizing the Performance of Your App, covers some tips, tricks, and
best practices for writing performant React Native code.
To get the most out of this book
It is assumed that you have the following levels of understanding:
You have some basic programming knowledge.
You are familiar with web development basics.
It will be helpful if you also have the following:
React, Vue, or Angular experience
Download the example code files
You can download the example code files for this book from your account at www.
packt.com. If you purchased this book elsewhere, you can visit www.packt.com/support
and register to have the files emailed directly to you.
You can download the code files by following these steps:
1. Log in or register at www.packt.com.
2. Select the SUPPORT tab.
3. Click on Code Downloads & Errata.
4. Enter the name of the book in the Search box and follow the onscreen
instructions.
Once the file is downloaded, please make sure that you unzip or extract the
folder using the latest version of:
WinRAR/7-Zip for Windows
Zipeg/iZip/UnRarX for Mac
7-Zip/PeaZip for Linux
The code bundle for the book is also hosted on GitHub at https://github.com/warlywa
re/react-native-cookbook. In case there's an update to the code, it will be updated on
the existing GitHub repository.
We also have other code bundles from our rich catalog of books and videos
available at https://github.com/PacktPublishing/. Check them out!
Download the color images
We also provide a PDF file that has color images of the screenshots/diagrams
used in this book. You can download it here: https://www.packtpub.com/sites/default/f
iles/downloads/9781788991926_ColorImages.pdf.
Conventions used
There are a number of text conventions used throughout this book.
CodeInText: Indicates code words in text, database table names, folder names,
filenames, file extensions, pathnames, dummy URLs, user input, and Twitter
handles. Here is an example: "We'll use a state object with a liked Boolean
property for this purpose."
A block of code is set as follows:
export default class App extends React.Component {
state = {
liked: false,
};
handleButtonPress = () => {
// We'll define the content on step 6
}
When we wish to draw your attention to a particular part of a code block, the
relevant lines or items are set in bold:
onst styles = StyleSheet.create({
container: {
flex: 1,
},
topSection: {
flexGrow: 3,
backgroundColor: '#5BC2C1',
alignItems: 'center',
},
Any command-line input or output is written as follows:
expo init project-name
Bold: Indicates a new term, an important word, or words that you see onscreen.
For example, words in menus or dialog boxes appear in the text like this. Here is
an example: "Click the Components tab, and install a simulator from the list of
provided simulators."
Warnings or important notes appear like this.
Tips and tricks appear like this.
Sections
In this book, you will find several headings that appear frequently (Getting
ready, How to do it..., How it works..., There's more..., and See also).
To give clear instructions on how to complete a recipe, use these sections as
follows:
Getting ready
This section tells you what to expect in the recipe and describes how to set up
any software or any preliminary settings required for the recipe.
How to do it…
This section contains the steps required to follow the recipe.
How it works…
This section usually consists of a detailed explanation of what happened in the
previous section.
There's more…
This section consists of additional information about the recipe in order to make
you more knowledgeable about the recipe.
See also
This section provides helpful links to other useful information for the recipe.
Get in touch
Feedback from our readers is always welcome.
General feedback: If you have questions about any aspect of this book, mention
the book title in the subject of your message and email us at
customercare@packtpub.com.
Errata: Although we have taken every care to ensure the accuracy of our
content, mistakes do happen. If you have found a mistake in this book, we would
be grateful if you would report this to us. Please visit www.packt.com/submit-errata,
selecting your book, clicking on the Errata Submission Form link, and entering
the details.
Piracy: If you come across any illegal copies of our works in any form on the
Internet, we would be grateful if you would provide us with the location address
or website name. Please contact us at copyright@packt.com with a link to the
material.
If you are interested in becoming an author: If there is a topic that you have
expertise in and you are interested in either writing or contributing to a book,
please visit authors.packtpub.com.
Reviews
Please leave a review. Once you have read and used this book, why not leave a
review on the site that you purchased it from? Potential readers can then see and
use your unbiased opinion to make purchase decisions, we at Packt can
understand what you think about our products, and our authors can see your
feedback on their book. Thank you!
For more information about Packt, please visit packt.com.
Setting Up Your Environment
The React Native ecosystem has evolved quite a bit since the first edition. The
open source tool Expo.io, in particular, has streamlined both the project
initialization and development phases, making working in React Native even
more of a pleasure than it already was in version 0.36.
With the Expo workflow, you'll be able to build native iOS and Android
applications using only JavaScript, work in the iOS simulator and Android
emulator with live reload, and effortlessly test your app on any real-world device
via Expo's app. Until you need access to native code (say, to integrate with
legacy native code from a separate code base), you can develop your application
entirely in JavaScript without ever needing to use Xcode or Android Studio. If
your project ever needs to evolve into native code support, Expo provides the
ability to detach your project, which changes your app into native code for use in
Xcode and Android Studio. For more information on detaching your Expo
project, please see Chapter 11, Adding Native Functionality – Part I .
Expo is an awesome way to build fully featured apps for Android and iOS
devices, without ever having to deal with native code. Let's get started!
We will cover the following topics in this chapter:
Installing dependencies
Initializing your first application
Running your application in a simulator/emulator
Running your application on a real device
Technical requirements
This chapter will cover installing the tools you'll be using throughout this book.
They include:
Expo
Xcode (for iOS simulator, macOS only)
Genymotion (for Android emulator)
Node.js
Watchman
Installing dependencies
The first step toward building our first React Native application is installing the
dependencies in order to get started.
Installing Xcode
As mentioned in the introduction of this chapter, Expo provides us with a
workflow in which we can avoid working in Xcode and Android Studio
altogether, so we can develop solely in JavaScript. However, in order to run your
app in the iOS simulator, you will need to have Xcode installed.
Xcode requires macOS, and therefore running your React Native application in an iOS
simulator is only possible on macOS.
Xcode should be downloaded from the App Store. You can search the App Store
for Xcode, or use the following link:
https://itunes.apple.com/app/xcode/id497799835.
Xcode is a sizable download, so expect this part to take a little while. Once you
have installed Xcode via the App Store, you can run it via the Applications folder
in the Finder:
1. This is the first screen you will see when launching Xcode. Note, if this is
the first time you've installed Xcode, you will not see recent projects listed
down the right-hand side:
2. From the menu bar, choose Xcode | Preferences... as follows:
3. Click the Components tab, and install a simulator from the list of provided
simulators:
4. Once installed, you can open the simulator from the menu bar: Xcode |
Open Developer Tool | Simulator:
Installing Genymotion
Genymotion Personal Edition is a free, feature-rich Android emulator
recommended by the Expo team. Genymotion also requires the VirtualBox
virtualizer, so we will install that first:
1. Download the VirtualBox application from https://www.virtualbox.org/wiki/Down
loads and run it. Follow the installation instructions:
2. Upon installation, you will likely be prompted by your OS to allow
VirtualBox to run:
3. You must follow these instructions for VirtualBox to work properly. From
the menu bar, choose the System Preferences... option from the Apple
menu:
4. Choose Security & Privacy from the System Preferences window:
5. On the General tab, you will see a message stating that software from
"Oracle America, Inc." was blocked. Click the Allow button next to this
message:
6. Once VirtualBox is installed, you must restart your machine! After
restarting, download Genymotion Personal Edition from https://www.genymotio
n.com/fun-zone/. You will need to make an account to download the software.
Once logged in, the site will allow you to download Genymotion from the
link provided previously. In the installer, drag Genymotion.app and Genymotion
Shell.app to the Applications folder:
7. Once the installation is complete, run Genymotion from
the Applications folder. Click the Personal Use link at the bottom of
the Usage notice dialog box:
8. Once you agree to the terms of use, you should be prompted to install a
virtual device. Select Yes:
9. The Expo documentation recommends using a Google Nexus 5 device,
along with any version of Android. In the Device model menu
choose Google Nexus 5, then select one of the Android versions to install.
I've chosen the newest version available:
10. Once the virtual device is downloaded and installed, you will be returned to
the main view. From here, choose the virtual device you've installed, and
click Start to run the emulator:
Installing Node.js
Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine, and is
designed to build scalable network applications. Node allows JavaScript to be
executed in a Terminal, and is an indispensable tool for any web developer. For
more information on what Node.js is, you can read the project's About Node.js
page at https://nodejs.org/en/about/.
According to the Expo installation documentation, Node.js is not technically
required, but as soon as you start actually building something, you'll want to
have it. Node.js itself is outside the scope of this book, but you can check out
the Further reading section at the end of this chapter for more resources on
working with Node.js.
There are numerous methods to install Node.js, and it is therefore difficult to
recommend a particular installation method. On macOS, you can install Node.js
in one of the following ways:
Downloading and installing Node.js from the project's site at https://nodejs.o
rg/en/download/.
Installing via Homebrew. If you are familiar with Homebrew, this process is
explained succinctly at https://medium.com/@katopz/how-to-install-specific-nodejs-
version-c6e1cec8aa11.
Installing via Node Version Manager
(NVM; https://github.com/creationix/nvm). NVM allows you to install multiple
versions of Node.js and easily switch between them. Use the instructions
provided in the repository's README to install NVM. This is the
recommended method, due to its flexibility, as long as you're comfortable
working in the Terminal.
Installing Expo
The Expo project used to have a GUI-based development environment called the
Expo XDE, which has been replaced with a browser-based GUI called the Expo
Developer Tools. Since the Expo XDE has been deprecated, creating new Expo
apps is now always done using the Expo CLI. This can be installed using npm
(Node Package Manager, which comes as part of Node.js) via the Terminal with
the following command:
npm install expo-cli -g
We'll be using Expo quite a bit throughout this book to create and build out
React Native applications, particularly those apps that do not need access to
native iOS or Android code. Applications built with Expo have some very nice
advantages for development, helping obfuscate native code, streamlining app
publishing and push notifications, and providing a lot of useful functionality
built into the Expo SDK. For more information on how Expo works, and how it
fits into the bigger picture of React Native development, see Chapter 10, App
Workflow and Third-Party Plugins.
Installing Watchman
Watchman is a tool used internally by React Native. Its purpose is to watch files
for updates, and trigger responses (such as live reloading) when changes occur.
The Expo documentation recommends installing Watchman, since it has been
reported that some macOS users have run into issues without it. The
recommended method for installing Watchman is via Homebrew. The missing
package manager for macOS, Homebrew allows you to install a wide array of
useful programs straight from your Terminal. It's an indispensable tool that
should be in every developer's tool bag:
1. If you don't have Homebrew installed already, run the following command
in the Terminal to install it (you can read more about it and view the official
documentation at https://brew.sh/):
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
2. Once Homebrew has been installed, run the following two commands in
Terminal to install watchman:
brew update
brew install watchman
Initializing your first app
The hard part of the setup is done! From here on out, we can use the magic
provided by Expo to easily create new apps for development.
We'll create our first app using Expo via the Expo CLI. Making a new
application is as simple as running the following:
expo init project-name
Running this command will first prompt you which type of app you'd like to
create: either a blank app, which has no functionality added, or a tabs app, which
will create a new app with minimal tab navigation. For the recipes in this book,
we'll be using the blank app option.
Once you've selected your preferred application type, a new, empty Expo-
powered React Native app in a new project-name directory is created, along with
all of the dependencies needed to start developing right away. All you need to do
is begin editing the App.js file in the new project directory to get to work.
To run our new app, we can cd into the directory, then use the expo start
command. This will automatically build and serve the app, and open a new
browser window with the Expo Developer Tools for your in-development React
Native app.
For a list of all of the available commands for the Expo CLI, check out the
documentation at https://docs.expo.io/versions/latest/guides/expo-cli.html.
With our first application created, let's move on to running the application in an
iOS simulator and/or Android emulator.
Running your app in a
simulator/emulator
You have created a new project, and started running that project with Expo in the
last step. Once we start making changes to our React Native code, wouldn't it be
nice to see the results of those changes? Thanks to Expo, running your project in
the installed iOS simulator or Android emulator has also been streamlined.
Running your app on an iOS
simulator
Thanks to Expo, running your app in the Xcode simulator only takes a few
clicks. To avoid potential issues, it's easiest to make sure your simulator is
already running before you attempt to open an Expo-based React Native app in a
simulator:
1. Open Xcode.
2. Open the Simulator from the menu bar: Xcode | Open Developer
Tool | Simulator:
3. Wait until the simulator has loaded the operating system:
4. In the Expo Developer Tools browser window, select Run on iOS
Simulator.
5. The first time you run a React Native app on the iOS simulator via Run on
iOS Simulator, the Expo app will be installed on the simulator, and your
app will automatically be opened within the Expo app. The simulated iOS
will ask if you want to Open in "Expo"?. Choose Open:
6. Upon loading, you will see the Expo Developer menu. You can toggle
between this menu and your React Native app by pressing command key +
D on your keyboard:
Running your app on an Android
emulator
We'll use the Android emulator recommended by the Expo team, Genymotion,
which we installed in a recipe earlier in this chapter. As with the iOS simulator,
ensure that your emulator is running before trying to launch the Expo-based app
in the emulator:
1. Open Genymotion.
2. Click the Start button:
3. The emulator will open and load the Android home screen. Note that
loading the emulator can take some time, so don't worry if it's taking a little
longer than you'd expect:
4. In the Expo Developer Tools browser window, select Run on Android
device/emulator.
5. In the Android emulator, you will be prompted to enable "Permit drawing
over other apps". Click OK:
6. Toggle on the Permit drawing over other apps option on the next screen:
7. Then hit the back button at the bottom of the emulator:
8. Once the JavaScript bundle is loaded, you will see your blank React Native
app in the emulator:
9. You can toggle between your React Native app and the Expo Developer
menu, a list of helpful features for development, by pressing command
key + M on your keyboard. The Expo Developer menu should look
something like this:
As you may have noticed, you have the option to Enable Hot Reloading. With
hot reloading enabled, the changes will update on the emulator/simulator right
away, without any need to reload the JavaScript bundle first.
Running your app on a real device
Expo also makes running your development app on a real device as easy as
running your app on a simulator. With the clever combination of the native Expo
app and a QR code, running on a real device is only a few clicks and taps away!
Since the process is the same for both iOS and Android, only the iOS process
will be covered in depth.
Running your app on an iPhone or
Android
You can get the in-development app running on your phone in three simple
steps:
1. Open the App Store on your iPhone, or the Google Play Store on your
Android device.
2. Search for and download the Expo Client app.
3. While your app is running on your development machine, you should also
have the Expo Developer Tools open in a browser. You should see a QR
code at the bottom of the left-hand side menu of the Expo Developer Tools.
Use the iPhone's native Camera app, or the Scan QR Code button in the
Expo Client app on Android, to scan the QR code. This will open your in-
development app on the device within the Expo Client app.
Your React Native app should now be running on your real device, fully
equipped with live reload! You can also shake the device to toggle between your
React Native app and the Expo Developer menu.
Summary
In this chapter, we've gone through all the steps required for getting started with
developing React Native apps, including initializing a new project, emulating
running your new project on your computer, and running your development app
on real-world devices. Thanks to the power of Expo, it's easier to jump in and
start working than ever before.
Now that you've got everything set up, it's time to start building!
Further reading
Here's a list of other resources covering similar topics:
The Expo installation documentation at https://docs.expo.io/versions/latest/int
roduction/installation.html.
Node.js Web Development at https://www.packtpub.com/mapt/book/web_development/9
781785881503.
Introducing Hot Reloading - React Native at https://facebook.github.io/react-n
ative/blog/2016/03/24/introducing-hot-reloading.html. This blog post from the
React Native team describes how Hot Reloading works in depth.
Publishing with Expo at https://docs.expo.io/versions/latest/guides/publishing.ht
ml. Expo has a publish feature that allows you to share your in-development
React Native application with fellow developers by creating a persistent
URL.
Expo Snack at https://snack.expo.io. Similar to codepen.io or jsfiddle.net, Snack
lets you live edit a React Native app in the browser!
Creating a Simple React Native App
In this chapter, we'll cover the following recipes:
Adding styles to elements
Using images to mimic a video player
Creating a toggle button
Displaying a list of items
Using flexbox to create a layout
Setting up and using navigation
React Native is a fast-growing library. Over the last year and a half before
writing this, it has become very popular among the open source community.
There's a new release every other week that improves performance, adds new
components, or provides access to new APIs on the device.
In this chapter, we'll learn about the most common components in the library. To
step through all of the recipes in this chapter, we'll have to create a new
application, so make sure you have your environment up and running.
Adding styles to elements
We have several components at our disposal, but containers and text are the most
common and useful components to create layouts or other components. In this
recipe, we'll see how to use containers and text, but most importantly we'll see
how styles work in React Native.
We'll create a UI for a simple music player; we won't be using icons for now, but
we'll add them later.
Getting ready
Follow the instructions in the previous chapter in order to create a new
application. We'll name this application fake-music-player.
When creating a new application with Expo, a small amount of boilerplate code
will be added to the App.js file in the root folder. This will be the starting point of
any React Native application you build. Feel free to remove all boilerplate at the
beginning of each recipe, as all code (including what's used in the App.js
boilerplate) will be discussed.
How to do it...
1. In the App.js file, we're going to create a stateless component. This
component will mimic a small music player. For now, it'll only display the
name of the song and a bar to show the progress. The first step is importing
our dependencies:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
2. Once we've imported the dependencies, we can build out the component:
export default class App extends React.Component {
render() {
const name = '01 - Blue Behind Green Bloches';
return (
<View style={styles.container}>
<View style={styles.innerContainer} />
<Text style={styles.title}>
<Text style={styles.subtitle}>Playing:</Text> {name}
</Text>
</View>
);
}
}
3. We have our component ready, so now we need to add some styles, to add
colors and fonts:
const styles = StyleSheet.create({
container: {
margin: 10,
marginTop: 100,
backgroundColor: '#e67e22',
borderRadius: 5,
},
innerContainer: {
backgroundColor: '#d35400',
height: 50,
width: 150,
borderTopLeftRadius: 5,
borderBottomLeftRadius: 5,
},
title: {
fontSize: 18,
fontWeight: '200',
color: '#fff',
position: 'absolute',
backgroundColor: 'transparent',
top: 12,
left: 10,
},
subtitle: {
fontWeight: 'bold',
},
});
4. As long as our simulator and emulator are running our application, we
should see the changes:
How it works...
In step 1, we included the dependencies of our component. In this case, we used
View, which is a container. If you're familiar with web development, View is similar
to div. We could add more Views inside other Views, Texts, Lists, and any other
custom component that we create or import from a third-party library.
As you can see, this is a stateless component, which means it doesn't have any
state; it's a pure function and doesn't support any of the life cycle methods.
We're defining a name constant in the component, but in real-world applications
this data should come from the props. In the return, we're defining the
JavaScript XML (JSX) that we're going to need to render our component,
along with a reference to the styles.
Each component has a property called style. This property receives an object
with all of the styles that we want to apply to the given component. Styles are
not inherited (except for the Text component) by the child components, which
means we need to set individual styles for each component.
In step 3, we defined the styles for our component. We're using the StyleSheet API
to create all of our styles. We could have used a plain object containing the
styles, but by using the StyleSheet API instead of an object, we gain some
performance optimizations, as the styles will be reused for every renderer, as
opposed to creating an object every time the render method gets executed.
There's more...
I'd like to call your attention to the definition of the title style in step 3. Here,
we've defined a property called backgroundColor and set transparent as its value. As a
good exercise, let's comment this line of code and see the result:
On iOS, the text will have an orange background color and it might not be what
we really want to happen in our UI. In order to fix this, we need to set the
background color of the text as transparent. But the question is, why is this
happening? The reason is that React Native adds some optimizations to the text
by setting the color from the parent's background color. This will improve the
rendering performance because the rendering engine won't have to calculate the
pixels around each letter of the text and the rendering will be executed faster.
Think carefully when setting the background color to transparent. If the component is going to
be updating the content very frequently, there might be some performance issues with text,
especially if the text is too long.
Using images to mimic a video player
Images are an important part of any UI, whether we use them to display icons,
avatars, or pictures. In this recipe, we'll use images to create a mock video
player. We'll also display the icons from the local device and a large image from
a remote server (hosted by Flickr).
Getting ready
In order to follow the steps in this recipe, let's create a new application. We're
going to name it fake-video-player.
We're going to display a few images in our application to mimic a video player,
so you'll need corresponding images for your application. I recommend using the
icons I used by downloading them from the repository for this recipe on GitHub
at https://github.com/warlyware/react-native-cookbook/tree/master/chapter-2/fake-video-pla
yer/images.
How to do it...
1. The first thing we're going to do is create a new folder called Images in the
root of the project. Add the images you've downloaded to the new folder.
2. In the App.js file, we include all of the dependencies we'll need for this
component:
import React from 'react';
import { StyleSheet, View, Image } from 'react-native';
3. We need to require the images that'll be displayed in our component. By
defining them in constants, we can use the same image in different places:
const playIcon = require('./images/play.png');
const volumeIcon = require('./images/sound.png');
const hdIcon = require('./images/hd-sign.png');
const fullScreenIcon = require('./images/full-screen.png');
const flower = require('./images/flower.jpg');
const remoteImage = { uri: `https://farm5.staticflickr.com/4702/24825836327_bb2e0fc39b_b.jpg` };
4. We're going to use a stateless component to render the JSX. We'll use all of
the images we've declared in the previous step:
export default class App extends React.Component {
render() {
return (
<View style={styles.appContainer}>
<ImageBackground source={remoteImage} style=
{styles.videoContainer} resizeMode="contain">
<View style={styles.controlsContainer}>
<Image source={volumeIcon} style={styles.icon} />
<View style={styles.progress}>
<View style={styles.progressBar} />
</View>
<Image source={hdIcon} style={styles.icon} />
<Image source={fullScreenIcon} style={styles.icon} />
</View>
</ImageBackground>
</View>
);
}
};
5. Once we have the elements that we're going to render, we need to define the
styles for each element:
const styles = StyleSheet.create({
flower: {
flex: 1,
},
appContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
videoContainer: {
backgroundColor: '#000',
flexDirection: 'row',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
controlsContainer: {
padding: 10,
backgroundColor: '#202020',
flexDirection: 'row',
alignItems: 'center',
marginTop: 175,
},
icon: {
tintColor: '#fff',
height: 16,
width: 16,
marginLeft: 5,
marginRight: 5,
},
progress: {
backgroundColor: '#000',
borderRadius: 7,
flex: 1,
height: 14,
margin: 4,
},
progressBar: {
backgroundColor: '#bf161c',
borderRadius: 5,
height: 10,
margin: 2,
paddingTop: 3,
width: 80,
alignItems: 'center',
flexDirection: 'row',
},
});
6. We're done! Now, when you view the application, you should see
something like the following:
How it works...
In step 2, we required the Image component. This is the component responsible for
rendering images from the local filesystem on the device or from a remote
server.
In step 3, we required all of the images. It's good practice to require the images
outside of the component in order to only require them once. On every renderer,
React Native will use the same image. If we were dealing with dynamic images
from a remote server, then we'd need to require them on every renderer.
The require function accepts the path of the image as a parameter. The path is
relative to the folder that our class is in. For remote images, we need to use an
object defining uri for where our file is.
In step 4, a stateless component was declared. We used remoteImage as the
background of our application via an ImageBackground element, since Image elements
cannot have child elements. This element acts similarly to the background-url
property in CSS.
The source property of Image accepts an object to load remote images or a
reference to the required file. It's very important to explicitly require every
image that we want to use because when we prepare our application for
distribution, images will be added to the bundle automatically. This is the reason
we should avoid doing anything dynamic, such as the following:
const iconName = playing ? 'pause' : 'play';
const icon = require(iconName);
The preceding code won't include the images in the final bundle. As a result,
we'll have errors when trying to access these images. Instead, we should refactor
our code to something like this:
const pause = require('pause');
const play = require('playing');
const icon = playing ? pause : play;
This way, the bundle will include both images when preparing our application
for distribution, and we can decide which image to display dynamically at
runtime.
In step 5, we defined the styles. Most of the properties are self-explanatory. Even
though the images we're using for icons are white, I've added the tintColor
property to show how it can be used to color images. Give it a try! Change
tintColor to #f00 and watch the icons turn red.
Flexbox is being used to align different portions of the layout. Flexbox in React
Native behaves essentially the same as it does in web development. We'll discuss
flexbox more in the Using flexbox to create a layout recipe later in this chapter,
but the complexities of flexbox itself are outside the scope of this book.
Creating a toggle button
Buttons are an essential UI component in every application. In this recipe, we'll
create a toggle button, which will be unselected by default. When the user taps
on it, we'll change the styles applied to the button to make it appear selected.
We'll learn how to detect the tap event, use an image as the UI, keep the state of
the button, and add styles based on the component state.
Getting ready
Let's create a new app. We're going to name it toggle-button. We're going to use
one image in this recipe. You can download the assets for this recipe from the
corresponding repository hosted on GitHub at https://github.com/warlyware/react-nat
ive-cookbook/tree/master/chapter-2/toggle-button/images.
How to do it...
1. We're going to create a new folder called images in the root of the project and
add the heart image to the new folder.
2. Let's import the dependencies for this class next:
import React, { Component } from 'react';
import {
StyleSheet,
View,
Image,
Text,
TouchableHighlight,
} from 'react-native';
const heartIcon = require('./images/heart.png');
3. For this recipe, we need to keep track of whether the button has been
pressed. We'll use a state object with a liked Boolean property for this
purpose. The initial class should look like this:
export default class App extends React.Component {
state = {
liked: false,
};
handleButtonPress = () => {
// Defined in a later step
}
render() {
// Defined in a later step
}
}
4. We need to define the content of our new component inside the render
method. Here, we're going to define the Image button and a Text element
underneath it:
export default class App extends React.Component {
state = {
liked: false,
};
handleButtonPress = () => {
// Defined in a later step
}
render() {
return (
<View style={styles.container}>
<TouchableHighlight
style={styles.button}
underlayColor="#fefefe"
>
<Image
source={heartIcon}
style={styles.icon}
/>
</TouchableHighlight>
<Text style={styles.text}>Do you like this app?</Text>
</View>
);
}
}
5. Let's define some styles to set dimensions, position, margins, colors, and so
on:
const styles = StyleSheet.create({
container: {
marginTop: 50,
alignItems: 'center',
},
button: {
borderRadius: 5,
padding: 10,
},
icon: {
width: 180,
height: 180,
tintColor: '#f1f1f1',
},
liked: {
tintColor: '#e74c3c',
},
text: {
marginTop: 20,
},
});
6. When we run the project on the simulators, we should have something
similar to the following screenshot:
7. In order to respond to the tap event, we need to define the content of the
handleButtonPress function and assign it as a callback to the onPress property:
handleButtonPress = () => {
this.setState({
liked: !this.state.liked,
});
}
render() {
return (
<View style={styles.container}>
<TouchableHighlight
onPress={this.handleButtonPress}
style={styles.button}
underlayColor="#fefefe"
>
<Image
source={heartIcon}
style={styles.icon}
/>
</TouchableHighlight>
<Text style={styles.text}>Do you like this app?</Text>
</View>
);
}
8. If we test our code, we won't see anything changing on the UI, even though
the state on the component changes when we press the button. Let's add a
different color to the image when the state changes. That way, we'll be able
to see a response from the UI:
render() {
const likedStyles = this.state.liked ? styles.liked : undefined;
return (
<View style={styles.container}>
<TouchableHighlight
onPress={this.handleButtonPress}
style={styles.button}
underlayColor="#fefefe"
>
<Image
source={heartIcon}
style={[styles.icon, likedStyles]}
/>
</TouchableHighlight>
<Text style={styles.text}>Do you like this app?</Text>
</View>
);
}
How it works...
In step 2, we imported the TouchableHighlight component. This is the component
responsible for handling the touch event. When the user touches the active area,
the content will be highlighted based on the underlayColor value we have set.
In step 3, we defined the state of Component. In this case, there's only one property
on the state, but we can add as many as needed. In Chapter 3, Implementing
Complex User Interfaces – Part I, we'll see more recipes about handling the state
in more complex scenarios.
In step 6, we used the setState method to change the value of the liked property.
This method is inherited from the Component class that we're extending.
In step 7, based on the current state of the liked property, we used the styles to set
the color of the image to red or we returned undefined to avoid applying any
styles. When assigning the styles to the Image component, we used an array to
assign many objects. This is very handy because the component will merge all of
the styles into one single object internally. The objects with the highest index
will overwrite the properties with the lowest object index in the array:
There's more...
In a real application, we're going to use several buttons, sometimes with an icon
aligned to the left, a label, different sizes, colors, and so on. It's highly
recommended to create a reusable component to avoid duplicating code all over
our app. In Chapter 3, Implementing Complex User Interfaces – Part I, we'll create
a button component to handle some of these scenarios.
Displaying a list of items
Lists are everywhere: a list of orders in the user's history, a list of available items
in a store, a list of songs to play. Nearly any application will need to display
some kind of information in a list.
For this recipe, we're going to display several items in a list component. We're
going to define a JSON file with some data, then we're going to load this file
using a simple require to finally render each item with a nice but simple layout.
Getting ready
Let's start by creating an empty app. We'll name this application list-items. We're
going to need an icon to display on each item. The easiest way to get images is
to download them from this recipe's repository hosted on GitHub at https://github
.com/warlyware/react-native-cookbook/tree/master/chapter-2/toggle-button/images.
How to do it...
1. We'll start by creating an images folder and adding basket.png to it. Also,
create an empty file in the root of the project called sales.json.
2. Inside the sales.json file, we'll define the data that we're going to display in
the list. Here's some sample data:
[
{
"items": 5,
"address": "140 Broadway, New York, NY 11101",
"total": 38,
"date": "May 15, 2016"
}
]
3. To avoid cluttering the pages of this book, I've only defined one record, but
go ahead and add more content to the array. Copying and pasting the same
object multiple times will do the trick. In addition, you could change some
values on the data so that each item displays unique data in the UI.
4. In our App.js file, let's import the dependencies we'll need:
import React, { Component } from 'react'; import { StyleSheet, View, ListView, Image, Text, } from 'react-native'; import data from './sales.json'; const basketIcon = require('./images/basket.png');
5. Now, we need to create the class to render the list of items. We're going to
keep the sales data on the state; that way, we could insert or remove
elements easily:
export default class App extends React.Component {
constructor(props) {
super(props);
const dataSource = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.state = {
dataSource: dataSource.cloneWithRows(data),
};
}
renderRow(record) {
// Defined in a later step
}
render() {
// Defined in a later step
}
}
6. In the render method, we need to define the ListView component and we'll use
the renderRow method to render each item. The dataSource property defines the
array of elements that we're going to render on the list:
render() {
return (
<View style={styles.mainContainer}>
<Text style={styles.title}>Sales</Text>
<ListView dataSource={this.state.dataSource} renderRow={this.renderRow} />
</View>
);
}
7. Now, we can define the contents of renderRow. This method receives each
object containing all of the information we need. We're going to display the
data in three columns. In the first column, we'll show an icon; in the second
column, we'll show the number of items for each sale and the address where
this order will ship; and the third column will display the date and the total:
return (
<View style={styles.row}>
<View style={styles.iconContainer}>
<Image source={basketIcon} style={styles.icon} />
</View>
<View style={styles.info}>
<Text style={styles.items}>{record.items} Items</Text>
<Text style={styles.address}>{record.address}</Text>
</View>
<View style={styles.total}>
<Text style={styles.date}>{record.date}</Text>
<Text style={styles.price}>${record.total}</Text>
</View>
</View>
);
8. Once we have the JSX defined, it's time to add the styles. First, we'll define
colors, margins, paddings, and so on for the main container, title, and row
container. In order to create the three columns for each row, we need to use
the flexDirection: 'row' property. We'll learn more about this property in a
later recipe in this chapter:
const styles = StyleSheet.create({
mainContainer: {
flex: 1,
backgroundColor: '#fff',
},
title: {
backgroundColor: '#0f1b29',
color: '#fff',
fontSize: 18,
fontWeight: 'bold',
padding: 10,
paddingTop: 40,
textAlign: 'center',
},
row: {
borderColor: '#f1f1f1',
borderBottomWidth: 1,
flexDirection: 'row',
marginLeft: 10,
marginRight: 10,
paddingTop: 20,
paddingBottom: 20,
},
});
9. If we refresh the simulators, we should see something similar to the
following screenshot:
10. Now, inside the StyleSheet definition, let's add styles for the icon. We're
going to add a yellow circle as the background and change the color of the
icon to white:
iconContainer: {
alignItems: 'center',
backgroundColor: '#feb401',
borderColor: '#feaf12',
borderRadius: 25,
borderWidth: 1,
justifyContent: 'center',
height: 50,
width: 50,
},
icon: {
tintColor: '#fff',
height: 22,
width: 22,
},
11. After this change, we'll see a nice icon on the left side of each row, as
shown in the following screenshot:
12. Finally, we'll add the styles for the text. We need to set color, size, fontWeight,
padding, and a few other properties:
info: {
flex: 1,
paddingLeft: 25,
paddingRight: 25,
},
items: {
fontWeight: 'bold',
fontSize: 16,
marginBottom: 5,
},
address: {
color: '#ccc',
fontSize: 14,
},
total: {
width: 80,
},
date: {
fontSize: 12,
marginBottom: 5,
},
price: {
color: '#1cad61',
fontSize: 25,
fontWeight: 'bold',
}
13. The end result should look similar to the following screenshot:
How it works...
In step 5, we created the data source and added data to the state. The
ListView.DataSource class implements performance data processing for the ListView
component. The rowHasChanged property is required, and it should be a function to
compare the next element.
When filling up the data source with data, we need to call the cloneWithRows
method and send an array of records.
If we want to add more data, we should call the cloneWithRows method again with
an array containing the previous and new data. The data source will make sure to
compute the differences and re-render the list as necessary.
In step 7, we define the JSX to render the list. Only two properties are required
for the list: the data source we already have from step 6 and renderRow.
The renderRow property accepts a function as a value. This function needs to return
the JSX for each row.
There's more...
We've created a simple layout using flexbox; however, there's another recipe in
this chapter where we'll dive into more detail about using flexbox.
Once we have our list, chances are that we're going to need to see the detail of
each order. You can use the TouchableHighlight component as the main container
for each row, so go ahead and give it a try. If you are not sure how to use the
TouchableHighlight component, take a look at the Creating a toggle button recipe
from earlier in this chapter.
Using flexbox to create a layout
In this recipe, we'll learn about flexbox. In the previous recipes in this chapter,
we've been using flexbox to create layouts, but in this recipe, we'll focus on the
properties we have at our disposal by recreating the layout from a random name
generator application on the App Store called Nominazer (https://itunes.apple.com/
us/app/nominazer/id765422087?mt=8).
Working in flexbox in React Native is essentially the same as working with
flexbox in CSS. This means if you're comfortable developing websites with a
flexbox layout, then you already know how to create layouts in React Native!
This exercise will cover the basics of working with flexbox in React Native, but
for a list of all of the layout props you can use, refer to the documentation on
Layout Props (https://facebook.github.io/react-native/docs/layout-props.html).
Getting ready
Let's begin by creating a new blank app. We'll name it flexbox-layout.
How to do it...
1. In App.js, let's import the dependencies we'll need for our app:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
2. Our application only needs a render method since we're building a static
layout. The rendered layout consists of a container View element and three
child View elements for each colored section of the app:
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<View style={styles.topSection}> </View>
<View style={styles.middleSection}></View>
<View style={styles.bottomSection}></View>
</View> );
}
}
3. Next, we can begin adding our styles. The first style we'll add will be
applied to the View element that wraps our entire app. Setting the flex
property to 1 will cause all children elements to fill all empty space:
const styles = StyleSheet.create({
container: {
flex: 1,
}
});
4. Now, we can add the styles for the three child View elements. Each section
has a flexGrow property applied to it, which dictates how much of the
available space each element should take up. topSection and bottomSection are
both set to 3, so they'll take up the same amount of space. Since the
middleSection has the flexGrow property set to 1, this element will take up one
third of the space that topSection and bottomSection take up:
topSection: {
flexGrow: 3,
backgroundColor: '#5BC2C1',
},
middleSection: {
flexGrow: 1,
backgroundColor: '#FFF',
},
bottomSection: {
flexGrow: 3,
backgroundColor: '#FD909E',
},
5. If we open our application in the simulators, we should already be able to
see the basic layout taking shape:
6. Here, we can add a Text element to each of the three child View elements we
created in step 2. Note the newly added code has been highlighted:
render() {
return (
<View style={styles.container}>
<View style={styles.topSection}>
<Text style={styles.topSectionText}>
4 N A M E S
</Text>
</View>
<View style={styles.middleSection}>
<Text style={styles.middleSectionText}>
I P S U M
</Text>
</View>
<View style={styles.bottomSection}>
<Text style={styles.bottomSectionText}>
C O M
</Text>
</View>
</View>
);
}
7. The text for each section defaults to the top-left corner of that section. We
can use flexbox to justify and align each of these elements to the desired
positions. All three child View elements have the alignItems flex property set
to 'center', which will cause the children of each element to be
centered along the x axis. justifyContent is used on the middle and bottom
sections to define how child elements should be justified along the y axis:
onst styles = StyleSheet.create({
container: {
flex: 1,
},
topSection: {
flexGrow: 3,
backgroundColor: '#5BC2C1',
alignItems: 'center',
},
middleSection: {
flexGrow: 1,
backgroundColor: '#FFF',
justifyContent: 'center',
alignItems: 'center',
},
bottomSection: {
flexGrow: 3,
backgroundColor: '#FD909E',
alignItems: 'center',
justifyContent: 'flex-end'
}
});
8. All that's left to be done is to add basic styles to the Text elements to
increase fontSize, fontWeight, and the required margin:
topSectionText: {
fontWeight: 'bold',
marginTop: 50
},
middleSectionText: {
fontSize: 30,
fontWeight: 'bold'
},
bottomSectionText: {
fontWeight: 'bold',
marginBottom: 30
}
9. If we open our application in simulators, we should be able to see our
completed layout:
How it works...
Our application is looking really good, and it was quite easy to accomplish by
using flexbox. We created three distinct sections by using View elements that take
up different fractions of the screen by setting the flexGrow properties to 3, 1, and 3,
respectively. This causes the top and bottom sections to be of equal vertical size,
and the middle section to be one third the size of the top and bottom.
When using flexbox, we have two directions to lay out child content, row and
column:
row: This allows us to arrange the children of the container horizontally.
column: This allows us to arrange the children of the container vertically. This
is the default direction in React Native.
When setting flex: 1 as we did with the container View element, we're telling that
element to take up all available space. If we were to remove flex: 1 or set flex to
0, we can see the layout collapse in on itself, since the container is no longer
flexing into all of the empty space:
Flexbox is great for supporting different screen resolutions as well. Even though
different devices may have different resolutions, we can ensure consistent
layouts that will look good on any device.
There's more...
There are some differences between how flexbox works in React Native and
how it works in CSS. First, the default flexDirection property in CSS is row,
whereas the default flexDirection property in React Native is column.
The flex property also behaves a bit differently in React Native. Instead of
setting flex to a string value, it can be set to a positive integer, 0, or -1. As the
official React Native documentation states:
When flex is a positive number, it makes the component flexible and it'll be sized proportional to its flex
value. So, a component with flex set to 2 will take twice the space as a component with flex set to 1. When
flex is 0, the component is sized according to width and height and is inflexible. When flex is -1, the
component is normally sized according width and height. However, if there's not enough space, the
component will shrink to its minWidth and minHeight.
There's a lot more to talk about flexbox, but for now we've gotten our feet wet.
In Chapter 3, Implementing Complex User Interfaces – Part I, we'll learn more
about layouts. We'll create a complex layout to use more of the available layout
properties.
See also
React Native Layout Props documentation (https://facebook.github.io/react-na
tive/docs/layout-props.html)
React Native Text Style Props documentation (https://facebook.github.io/react
-native/docs/text-style-props.html)
Yoga (https://github.com/facebook/yoga)—Facebook's Flexbox implementation
utilized by React Native
An excellent Stack Overflow post that covers how React Native flex
properties work, with examples—https://stackoverflow.com/questions/43143258/fl
ex-vs-flexgrow-vs-flexshrink-vs-flexbasis-in-react-native
Setting up and using navigation
For any application that has more than one view, a navigation system is of
paramount importance. The need for navigation is so pervasive in application
development that Expo provides two templates when you create a new
application: Blank or Tab Navigation. This recipe is based on a very pared
down version of the Tab Navigation app template provided by Expo. We'll still
begin the recipe with a Blank app and build our basic Tab Navigation app from
scratch to better understand all of the requisite parts. After completing this
recipe, I encourage you to start a new app with the Tab Navigation template to
see some of the more advanced features we'll be covering in later chapters,
including push notifications and stack navigation.
Getting ready
Let's go ahead and create a new blank application named simple-navigation. We're
also going to need a third-party package for handling our navigation. We'll be
using the react-navigation package. In the Terminal, navigate to the root of the
new project and install this package with the following command:
yarn add react-navigation
That's all of the setup we need. Let's build!
How to do it...
1. Inside the App.js file, let's import our dependencies:
import React from 'react';
import { StyleSheet, View } from 'react-native';
2. The App component for this app will be very simple. We just need an App
class with a render function that renders our app container. We'll also add
styles for filling the window and adding a white background:
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
}
});
3. The next step for App.js will be to import and use the MainTabNavigator
component, which is a new component that we'll create in step 4:
import MainTabNavigator from './navigation/MainTabNavigator';
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<MainTabNavigator />
</View>
);
}
}
4. We'll need to create a new file for our MainTabNavigator component. Let's
create a new folder in the root of the project called navigation. In this new
folder, we'll create MainTabNavigator.js for our navigation component.
5. In MainTabNavigator.js, we can import all of the dependencies we need for
navigation. The dependencies include three screens (HomeScreen,LinksScreen,
and SettingsScreen). We'll add these screens in later steps:
import React from 'react';
import { Ionicons } from '@expo/vector-icons';
import { TabNavigator, TabBarBottom } from 'react-navigation';
import HomeScreen from '../screens/HomeScreen';
import LinksScreen from '../screens/LinksScreen';
import SettingsScreen from '../screens/SettingsScreen';
6. Our navigation component will use the TabNavigator method provided by
react-navigation for defining the routes and navigation for our
app. TabNavigator takes two parameters: a RouteConfig object to define each
route and a TabNavigatorConfig object to define the options for our TabNavigator
component:
export default TabNavigator({
// RouteConfig, defined in step 7.
}, {
// TabNavigatorConfig, defined in steps 8 and 9.
});
7. First, we'll define the RouteConfig object, which will create a route map for
our application. Each key in the RouteConfig object serves as the name of the
route. We set the screen property for each route to the corresponding screen
component we want to be displayed on that route:
export default TabNavigator({
Home: {
screen: HomeScreen,
},
Links: {
screen: LinksScreen,
},
Settings: {
screen: SettingsScreen,
},
}, {
// TabNavigatorConfig, defined in steps 8 and 9.
});
8. TabNavigatorConfig has a little more to it. We pass the TabBarBottom component
provided by react-navigation to the tabBarComponent property to declare what
kind of tab bar we want to use (in this case, a tab bar designed for the
bottom of the screen). tabBarPosition defines whether the bar is on the top or
bottom of the screen. animationEnabled specifies whether transitions are
animated, and swipeEnabled declares whether views can be changed via
swiping:
export default TabNavigator({
// Route Config, defined in step 7.
}, {
navigationOptions: ({ navigation }) => ({
// navigationOptions, defined in step 9.
}),
tabBarComponent: TabBarBottom,
tabBarPosition: 'bottom',
animationEnabled: false,
swipeEnabled: false,
});
9. In the navigationOptions property of the TabNavigatorConfig object, we'll define
dynamic navigationOptions for each route by declaring a function that takes
the navigation prop for the current route/screen. We can use this function to
decide how the tab bar will behave per route/screen, since it's designed to
return an object that sets navigationOptions for the appropriate screen. We'll
use this pattern to define the appearance of the tabBarIcon property for each
route:
navigationOptions: ({ navigation }) => ({
tabBarIcon: ({ focused }) => {
// Defined in step 10
},
}),
10. The tabBarIcon property is set to a function whose parameters are the props
for the current route. We'll use the focused prop to decide whether to render a
colored in icon or an outlined icon, depending on the current route. We
get routeName from the navigation prop via navigation.state, define icons for
each of our three routes, and return the rendered icon for the appropriate
route. We'll use the Ionicons component provided by Expo to create each
icon and define the icon's color based on whether the icon's route is focused:
navigationOptions: ({ navigation }) => ({
tabBarIcon: ({ focused }) => {
const { routeName } = navigation.state;
let iconName;
switch (routeName) {
case 'Home':
iconName = `ios-information-circle`;
break;
case 'Links':
iconName = `ios-link`;
break;
case 'Settings':
iconName = `ios-options`;
}
return (
<Ionicons name={iconName}
size={28} style={{marginBottom: -3}}
color={focused ? Colors.tabIconSelected :
Colors.tabIconDefault}
/>
);
},
}),
11. The last step in setting up MainTabNavigator is to create the Colors constant used
to color each icon:
const Colors = {
tabIconDefault: '#ccc',
tabIconSelected: '#2f95dc',
}
12. Our routing is now complete! All that's left now is to create the three screen
components for each of the three routes we imported and defined
in MainTabNavigator.js. For simplicity's sake, each of the three screens will
have identical code, except for background color and identifying text.
13. In the root of the project, we need to create a screens folder to house our
three screens. In the new folder, we'll need to
make HomeScreen.js,LinksScreen.js, and SettingsScreen.js.
14. Let's start by opening the newly created HomeScreen.js and adding the
necessary dependencies:
import React from 'react';
import {
StyleSheet,
Text,
View,
} from 'react-native';
15. The HomeScreen component itself is quite simple, just a full color page with
the word Home in the middle of the screen to show which screen we're
currently on:
export default class HomeScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.headline}>
Home
</Text>
</View>
);
}
}
16. We'll also need to add the styles for our Home screen layout:
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#608FA0',
},
headline: {
fontWeight: 'bold',
fontSize: 30,
color: 'white',
}
});
17. All that's left now is to repeat step 14, step 15, and step 16 for the remaining
two screens, along with some minor changes. LinksScreen.js should look like
HomeScreen.js with the following highlighted sections updated:
import React from 'react';
import {
StyleSheet,
Text,
View,
} from 'react-native';
export default class LinksScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.headline}>
Links
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F8759D',
},
headline: {
fontWeight: 'bold',
fontSize: 30,
color: 'white',
}
});
18. Similarly, inside SettingsScreen.js, we can create the third screen component
using the same structure as the previous two screens:
import React from 'react';
import {
StyleSheet,
Text,
View,
} from 'react-native';
export default class SettingsScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.headline}>
Settings
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F0642E',
},
headline: {
fontWeight: 'bold',
fontSize: 30,
color: 'white',
}
});
19. Our application is complete! When we view our application in the
simulator, it should have a tab bar along the bottom of the screen that
transitions between our three routes:
How it works...
In this recipe, we covered one of the most common and fundamental navigation
patterns in native apps, the tab bar. The React Navigation library is a very robust,
feature rich navigation solution and will likely be able to provide your app with
any kind of navigation needed. We'll cover more uses of React Navigation in Chap
ter 3, Implementing Complex User Interfaces.
See also
React Navigation official documentation (https://reactnavigation.org/)
Expo's guide on routing and navigation (https://docs.expo.io/versions/latest/gu
ides/routing-and-navigation.html)
Implementing Complex User
Interfaces - Part I
In this chapter, we will implement complex user interfaces. We will learn more
about using flexbox to create components that work on different screen sizes,
how to detect orientation changes, and more.
The chapter will cover the following recipes:
Creating a reusable button with theme support
Building a complex layout for tablets using flexbox
Including custom fonts
Using font icons
Creating a reusable button with
theme support
Reusability is very important when developing software. We should avoid
repeating the same thing over and over again, and instead we should create small
components that we can reuse as many times as needed.
In this recipe, we will create a Button component, and we are also going to define
several properties to change its look and feel. While going through this recipe,
we will learn how to use properties and how to dynamically apply different
styles to a component.
Getting ready
We need to create an empty app. Let's name it reusable-button.
How to do it...
1. In the root of our new app, we'll need to create a new Button folder for our
reusable button-related code. Let's also create index.js and styles.js in our
new Button folder.
2. We will start by importing the dependencies for our new component. In the
Button/index.js file, we will be creating a Button component. This means we'll
need to import the Text and TouchableOpacity components. You'll notice we're
also importing styles that do not exist yet. We will define these styles in a
different file later in this recipe. In the Button/index.js file, we should have
these imports:
import React, { Component } from 'react';
import {
Text,
TouchableOpacity,
} from 'react-native';
import {
Base,
Default,
Danger,
Info,
Success
} from './styles';
3. Now that we have our dependencies imported, let's define the class for this
component. We are going to need some properties and two methods. It's
also required that we export this component so we can use it elsewhere:
export default class Button extends Component {
getTheme() {
// Defined in a later step
}
render() {
// Defined in a later step
}
}
4. We need to select the styles to apply to our component based on the given
properties. For this purpose, we will define the getTheme method. This method
will check whether any of the properties are true and will return the
appropriate styles. If none are true, it will return the Default style:
getTheme() {
const { danger, info, success } = this.properties;
if (info) {
return Info;
}
if (success) {
return Success;
}
if (danger) {
return Danger;
}
return Default;
}
5. It's required that all components have a render method. Here, we need to
return the JSX elements for this component. In this case, we will get the
styles for the given properties and apply them to the TouchableOpacity
component.
We are also defining a label for the button. Inside this label, we will render
the children property. If a callback function is received, then it will be
executed when the user presses this component:
render() {
const theme = this.getTheme();
const {
children,
onPress,
style,
rounded,
} = this.properties;
return (
<TouchableOpacity
activeOpacity={0.8}
style={[
Base.main,
theme.main,
rounded ? Base.rounded : null ,
style,
]}
onPress={onPress}
>
<Text style={[Base.label, theme.label]}>{children}</Text>
</TouchableOpacity>
);
}
6. We are almost done with our Button component. We still need to define our
styles, but first let's move over to the App.js file in the root of the project. We
need to import the dependencies, including the Button component we have
created.
We are going to display an alert message when the user clicks the button,
therefore, we also need to import the Alert component:
import React from 'react';
import {
Alert,
StyleSheet,
View
} from 'react-native';
import Button from './Button';
7. Once we have all the dependencies, let's define a stateless component that
renders a few buttons. The first button will use the default theme, and the
second button will use the success style, which will add a nice green color
to the button's background. The last button will display an alert when it gets
pressed. For that, we need to define the callback function that will use the
Alert component, just setting the title and message:
export default class App extends React.Component {
handleButtonPress() {
Alert.alert('Alert', 'You clicked this button!');
}
render() {
return(
<View style={styles.container}>
<Button style={styles.btn}>
My first button
</Button>
<Button success style={styles.btn}>
Success button
</Button>
<Button info style={styles.btn}>
Info button
</Button>
<Button danger rounded style={styles.btn}
onPress={this.handleButtonPress}>
Rounded button
</Button>
</View>
);
}
}
8. We are going to add some styles for how the main layout should align and
justify each button, along with some margins:
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
btn: {
margin: 10,
},
});
9. If we try to run the app now, we will get some errors. This is because we
haven't declared the styles for our button. Let's work on that now. Inside the
Button/styles.js file, we need to define the base styles. These styles will be
applied to every instance of the button. Here, we will define a radius,
padding, font color, and all the common styles that we need for this
component:
import { StyleSheet } from 'react-native';
const Base = StyleSheet.create({
main: {
padding: 10,
borderRadius: 3,
},
label: {
color: '#fff',
},
rounded: {
borderRadius: 20,
},
});
10. Once we have the common styles for our button, we need to define the
styles for the Danger, Info, Success, and Default themes. For this, we are going
to define different objects for each theme. Inside each theme, we will use
the same object but with specific styles for that theme.
To keep things simple, we are only going to change the backgroundColor, but
we do have the option to use as many style properties as we want:
const Danger = StyleSheet.create({
main: {
backgroundColor: '#e74c3c',
},
});
const Info = StyleSheet.create({
main: {
backgroundColor: '#3498db',
},
});
const Success = StyleSheet.create({
main: {
backgroundColor: '#1abc9c',
},
});
const Default = StyleSheet.create({
main: {
backgroundColor: 'rgba(0 ,0 ,0, 0)',
},
label: {
color: '#333',
},
});
11. Finally, let's export the styles. This step is necessary so that the Button
component can import all the styles for each theme:
export {
Base,
Danger,
Info,
Success,
Default,
};
12. If we open the app, we should be able to see our completed layout:
How it works...
In this example, we made use of the TouchableOpacity component. This component
allows us to define a nice animation that changes the opacity when the user
presses the button.
We can use the activeOpacity property to set the opacity value when the button
gets pressed. The value can be any number between 0 and 1, where 0 is
completely transparent.
If we press the rounded button, we will see a native Alert message, as shown in
the following screenshot:
Building a complex layout for tablets
using flexbox
Flexbox is really convenient when it comes to creating responsive layouts. React
Native uses flexbox as a layout system, and if you are already familiar with these
concepts, it will be really easy for you to start creating layouts of any kind.
As discussed in the previous chapter, there are some differences between the
way flexbox works in React Native as compared to how it works in CSS. For
more information on the differences between React Native and CSS flexbox,
please refer to the How it works... section of the Using flexbox to create a
layout recipe in Chapter 2, Creating a Simple React Native App.
In this recipe, we will create a layout to display a list of blog posts. Each post
will be a small card with an image, an excerpt, and a button to read more. We
will use flexbox to arrange the posts on the main container based on screen size.
This will allow us to handle the screen rotation by properly aligning the cards in
both landscape and portrait.
Getting ready
In order to follow the steps in this recipe, it is necessary to create an empty app
using the React Native CLI. We are going to name the new app tablet-flexbox.
When we create a new app with Expo, there is an app.json that gets created at the
base of the project that provides some basic configuration. In this recipe, we are
building an app that we want to be sure looks good on a tablet, particularly in
landscape mode. When we open app.json, we should see an orientation property
set to 'portrait'. This property determines which orientations should be allowed
within our app. The orientation property accepts 'portrait' (lock app to portrait
mode), 'landscape' (lock app to landscape mode), and 'default' (allow app to
adjust screen orientation based on the device's orientation). For our app, we will
set the orientation to 'landscape' so that we can support both landscape and portrait
layouts.
We'll also be using some images, which need to be hosted remotely for this
recipe to properly simulate loading remote data and displaying images with
the Image component. I have uploaded these images to the www.imgur.com image
hosting service, and referenced these remote images in the data.json file that the
recipe uses for its consumable data. If, for any reason, these remote images don't
load properly for you, they are also in included in the repository for this recipe,
under the /assets folder. Feel free to upload them to any server or hosting service,
and update the image URLs in data.json accordingly. The repository can be found
on GitHub at https://github.com/warlyware/react-native-cookbook/tree/master/chapter-3/ta
blet-flexbox.
How to do it...
1. First, we need to create a Post folder in the root of the project. We need to
also create an index.js and a styles.js file in the new Post folder. We will use
this Post component to display each post for our app. Finally, we need to add
a data.json file to the root of the project, which we will use to define a list of
posts.
2. Now we can move on to building the App.js component. First, we need to
import the dependencies for this class. We are going to use a ListView
component to render the list of posts. We'll also need Text and
View components for content containers. We are going to create a custom Post
component to render each post on the list; we also need to import the
data.json file:
import React, { Component } from 'react';
import { ListView, StyleSheet, Text, View } from 'react-native';
import Post from './Post';
import data from './data.json';
3. Let's create the class for the App component. Here, we will use the data from
the .json file to create the dataSource for the list. We will add some actual data
to our data.json file in the next step. In the render method, we are going to
define a simple top toolbar and the List component. We are going to use the
Post component for every record and get the dataSource from the state.
If you have any questions regarding the ListView component, you should
take a look at the recipe in Chapter 2, Creating a Simple React Native App,
where we created a list of orders:
const dataSource = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
});
export default class App extends Component {
state = {
dataSource: dataSouce.cloneWithRows(data.posts),
};
render() {
return (
<View style={styles.container}>
<View style={styles.toolbar}>
<Text style={styles.title}>Latest posts</Text>
</View>
<ListView
dataSource={this.state.dataSource}
renderRow={post => <Post {...post} />}
style={styles.list}
contentContainerStyle={styles.content}
/>
</View>
);
}
}
4. Two files are still missing: the .json file with the data and the Post
component. In this step, we will create the data that we are going to use for
each post. To make things simple, there is only one record of data in the
following code snippet, but the rest of the POST object I used in this recipe
can be found in the data.json file of the code repository for this recipe:
{
"posts": [
{
"title": "The Best Article Ever Written",
"img": "https://i.imgur.com/mf9daCT.jpg",
"content": "Lorem ipsum dolor sit amet...",
"author": "Bob Labla"
},
// Add more records here.
]
}
5. Now that we have some data, we are ready to work on the Post component.
In this component, we need to display the image, title, and button. Since
this component does not need to know about state, we will use a stateless
component. The following code uses all the components we know from Chap
ter 2, Creating a Simple React Native App. If something is unclear, please
review that chapter again.
This component receives the data as a parameter, which we then use for
displaying the content in the component. The Image component will use
the img property defined on each object in the data.json file to display the
remote image:
import React from 'react';
import {
Image,
Text,
TouchableOpacity,
View
} from 'react-native';
import styles from './styles';
const Post = ({ content, img, title }) => (
<View style={styles.main}>
<Image
source={{ uri: img }}
style={styles.image}
/>
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<Text>{content}</Text>
</View>
<TouchableOpacity style={styles.button} activeOpacity={0.8}>
<Text style={styles.buttonText}>Read more</Text>
</TouchableOpacity>
</View>
);
export default Post;
6. Once we have defined the component, we also need to define the styles for
each post. Let's create an empty StyleSheet export so that the Post component
relying on styles.js will properly function:
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
// Defined in later steps
});
export default styles;
7. If we try to run the app, we should be able to see the data from the .json file
on the screen. It won't be very pretty though, since, we haven't applied any
styles yet:
8. We have everything we need on the screen. Now we are ready to start
working on the layout. First, let's add styles for our Post container. We'll be
setting width, height, borderRadius, and a few others. Let's add them to
the /Post/styles.js file:
const styles = StyleSheet.create({
main: {
backgroundColor: '#fff',
borderRadius: 3,
height: 340,
margin: 5,
width: 240,
}
});
9. By now, we should see small boxes vertically aligned. That's some
progress, but we need to add more styles to the image so we can see it
onscreen. Let's add an image property to the same styles const from the last
step. The resizeMode property will allow us to set how we want to resize the
image. In this case, by selecting cover, the image will keep the aspect ratio
of the original:
image: {
backgroundColor: '#ccc',
height: 120,
resizeMode: 'cover',
}
10. For the content of the post, we want to take all the available height on the
card, therefore we need to make it flexible and add some padding. We'll
also add overflow: hidden to the content to avoid overflowing the View element.
For the title, we only need to change the fontSize and add a margin to the
bottom:
content: {
padding: 10,
overflow: 'hidden',
flex: 1,
},
title: {
fontSize: 18,
marginBottom: 5,
},
11. Finally, for the button, we will set the backgroundColor to green and the text to
white. We also need to add some padding and margin for spacing:
button: {
backgroundColor: '#1abc9c',
borderRadius: 3,
padding: 10,
margin: 10,
},
buttonText: {
color: '#fff',
textAlign: 'center',
}
12. If we refresh the simulator, we should see our posts in small cards.
Currently, the cards are arranged vertically, but we want to render all of
them horizontally. We are going to fix that in the following steps:
Primary styles have been added for all post elements
13. Currently, we can only see the first three items on the list in a column,
instead of in a row across the screen. Let's return to the App.js file and start
adding our styles. We add flex: 1 to the container so that our layout will
always fill the screen. We also want to show a toolbar at the top. For that,
we just need to define some padding and color as follows:
const styles = StyleSheet.create({
container: {
flex: 1,
},
toolbar: {
backgroundColor: '#34495e',
padding: 10,
paddingTop: 20,
},
title: {
color: '#fff',
fontSize: 20,
textAlign: 'center',
}
});
14. Let's add some basic styles to the list as well. Just a nice background color
and some padding. We'll also add the flex property, which will ensure the
list takes all the available height on the screen. We only have two
components here: the toolbar and the list. The toolbar is taking about 50 px.
If we make the list flexible, it will take all of the remaining available space,
which is exactly what we want when rotating the device or when running
the app in different screen resolutions:
list: {
backgroundColor: '#f0f3f4',
flex: 1,
paddingTop: 5,
paddingBottom: 5,
}
15. If we check the app in the simulator once more, we should be able to see
the toolbar and list being laid out as expected:
Styles applied to each post to give them a card like appearance
16. We are almost done with this app. All we have left to do is to arrange the
cards horizontally. This can be achieved with flexbox in three simple steps:
content: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-around',
},
The first step is applying these content styles via the contentContainerStyle
property in the ListView component. Internally, the ListView component will
apply these styles to the content container, which wraps all of the child
views.
We then set the flexDirection to row. This will horizontally align the cards
on the list; however, this presents a new problem: we can only see one
single row of posts. To fix the problem, we need to wrap the items. We
do this by setting the flexWrap property to wrap, which will automatically
move the items that don't fit in the view to the next row. Lastly, we use
the justifyContent property and set it to center, which will center our
ListView in the middle of our app.
17. We now have a responsive app that looks good on a tablet in landscape
mode:
Side-by-side comparison of iPad and Android tablet screenshots in landscape mode
And looks just as good in portrait mode:
Side-by-side comparison of iPad and Android tablet screenshots in portrait mode
There's more...
Expo also provides a ScreenOrientation helper for changing the orientation
configuration of the app. This helper also allows for more granular orientation
settings (such as ALL_BUT_UPSIDE_DOWN or LANDSCAPE_RIGHT). If your app needs dynamic,
granular control over screen orientation, see the ScreenOrientation Expo
documentation for information: https://docs.expo.io/versions/v24.0.0/sdk/screen-orien
tation.html.
See also
Official documentation on static image resources and the <Image> component can
be found at https://facebook.github.io/react-native/docs/images.html.
Including custom fonts
At some point, we are probably going to want to display text with a custom font
family. Until now, we've been using the default font, but we can use any other
that we like.
Before Expo, the process of adding custom fonts was more difficult, required
working with native code, and needed to be implemented differently in iOS and
Android. Luckily, through the use of Expo's font helper library, this has become
streamlined and simplified
In this recipe, we will import a few fonts and then display text using each of the
imported font families. We will also use different font styles, such as bold and
italic.
Getting ready
In order to work on this example, we need some fonts. You can use whatever
fonts you want. I recommend going to Google Fonts (https://fonts.google.com/)
and downloading your favorites. For this recipe, we will be using the Josefin
Sans and Raleway fonts.
Once you have the fonts downloaded, let's create an empty app and name it
custom-fonts. When we create a blank app with Expo, it creates an assets folder in
the root of the project for placing all of your assets (images, fonts, and so on), so
we'll follow the standard and add our fonts to this folder. Let's create the
/assets/fonts folder and add our custom font files downloaded from Google Fonts.
When downloading fonts from Google Fonts, you'll get a .zip file containing a
.ttf file for each of the font family variants. We will be using the regular, bold,
and italic variations, so copy the corresponding .ttf files for each variant in each
family to our /assets/fonts folder.
How to do it...
1. With our font files in place, the first step is to open App.js and the imports
we'll need:
import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { Font } from 'expo';
2. Next, we'll add a simple component for displaying some text that we want
to style with our custom fonts. We'll start with just one Text element to
display the regular variant of the Roboto font:
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.josefinSans}>
Hello, Josefin Sans!
</Text>
</View>
);
}
}
3. Let's also add some starter styles for the component we've just created. For
now, we'll just increase the font size for our josefinSans class styles:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
josefinSans: {
fontSize: 40,
}
});
4. If we open the app now in our simulator, we will see the Hello, Josefin
Sans! text displayed in the middle of the screen using the default font:
5. Let's load our JosefinSans-Regular.ttf font file so that we can style our text
with it. We'll use the componentDidMount life cycle hook provided by React
Native to tell our app when to start loading the font:
export default class App extends React.Component {
componentDidMount() {
Font.loadAsync({
'josefin-sans-regular': require('./assets/fonts/JosefinSans-Regular.ttf'),
});
}
render() {
return (
<View style={styles.container}>
<Text style={styles.josefinSans}>
Hello, Josefin Sans!
</Text>
</View>
);
}
}
6. Next, we'll add the font we're loading to the styles being applied to our Text
element:
const styles = StyleSheet.create({
// Other styles from step 3
josefinSans: {
fontSize: 40,
fontFamily: 'josefin-sans-regular'
}
});
7. Boom, We now have styles, right? Well, not quite. If we look back at our
simulators, we'll see that we're getting an error instead:
console.error: "fontFamily 'josefin-sans-regular' is not a system font and has not been loaded through Expo.Font.loadAsync"
8. But we did just load fonts via Expo.Font.loadAsync! What gives? It turns out we
have a race condition on our hands. The josefinSans styles we defined for our
Text element are being applied before the Josefin Sans font has been loaded.
To handle this problem, will need to use the component's state to keep track
of the load status of the font:
export default class App extends React.Component {
state = {
fontLoaded: false
};
9. Now that our component has a state, we can update the state's fontLoaded
property to true once the font is loaded. Using the ES6 feature async/await
makes this succinct and straightforward. Let's do this in our componentDidMount
code block:
async componentDidMount() {
await Font.loadAsync({
'josefin-sans-regular': require('./assets/fonts/JosefinSans-
Regular.ttf'),
});
}
10. Since we are now awaiting the Font.loadAsync() call, we can set the state of
fontLoaded to true once the call is complete:
async componentDidMount() {
await Font.loadAsync({
'josefin-sans-regular': require('./assets/fonts/JosefinSans-
Regular.ttf'),
});
this.setState({ fontLoaded: true });
}
11. All that's left to do is to update our render method to only render the Text
element depending on the custom font when the fontLoaded state property is
true:
<View style={styles.container}>
{
this.state.fontLoaded ? (
<Text style={styles.josefinSans}>
Hello, Josefin Sans!
</Text>
) : null
}
</View>
12. Now, when we check out our app in the simulators, we should see our
custom font being applied:
13. Let's load the rest of our fonts so that we can use them in our app as well:
await Font.loadAsync({
'josefin-sans-regular': require('./assets/fonts/JosefinSans-
Regular.ttf'),
'josefin-sans-bold': require('./assets/fonts/JosefinSans-
Bold.ttf'),
'josefin-sans-italic': require('./assets/fonts/JosefinSans-
Italic.ttf'),
'raleway-regular': require('./assets/fonts/Raleway-
Regular.ttf'),
'raleway-bold': require('./assets/fonts/Raleway-Bold.ttf'),
'raleway-italic': require('./assets/fonts/Raleway-
Italic.ttf'),
});
14. We'll also need Text elements for displaying text in each of our new font
families/variants. Note that we'll also need to wrap all our Text elements in
another View element, since JSX expressions require that there be only one
parent node. We're also now passing the style property an array of styles to
apply in order to consolidate the fontSize and padding styles we'll be applying
in the next step:
render() {
return (
<View style={styles.container}>
{
this.state.fontLoaded ? (
<View style={styles.container}>
<Text style={[styles.josefinSans,
styles.textFormatting]}>
Hello, Josefin Sans!
</Text>
<Text style={[styles.josefinSansBold,
styles.textFormatting]}>
Hello, Josefin Sans!
</Text>
<Text style={[styles.josefinSansItalic,
styles.textFormatting]}>
Hello, Josefin Sans!
</Text>
<Text style={[styles.raleway, styles.textFormatting]}>
Hello, Raleway!
</Text>
<Text style={[styles.ralewayBold,
styles.textFormatting]}>
Hello, Raleway!
</Text>
<Text style={[styles.ralewayItalic,
styles.textFormatting]}>
Hello, Raleway!
</Text>
</View>
) : null
}
</View>
);
}
15. All that's left to apply our custom fonts is to add the new styles to the
StyleSheet:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
josefinSans: {
fontFamily: 'josefin-sans-regular',
},
josefinSansBold: {
fontFamily: 'josefin-sans-bold',
},
josefinSansItalic: {
fontFamily: 'josefin-sans-italic',
},
raleway: {
fontFamily: 'raleway-regular',
},
ralewayBold: {
fontFamily: 'josefin-sans-bold'
},
ralewayItalic: {
fontFamily: 'josefin-sans-italic',
},
textFormatting: {
fontSize: 40,
paddingBottom: 20
}
});
16. Now, in our app, we'll see six different text elements, each styled with its
own custom font:
How it works...
In step 5 and step 6, we used the componentDidMount React life cycle hook to tell our
app when to load. While it may seem tempting to use componentWillMount, this too
will throw an error, since componentWillMount is not guaranteed to wait for our
Font.loadAsync to finish. By using componentDidMount, we can also assure we are not
blocking the initial rendering of the app.
In step 9, we used the ES6 feature async/await. You're likely familiar with this
pattern if you're a web developer, but if you'd like more information, I've
included an awesome article from ponyfoo.com in the See also section at the end of
this recipe, which does a great job of explaining how async/await works.
In step 11, we used a ternary statement to render either our custom font styled
Text element if loaded, or to render nothing if it's not loaded by returning null.
Fonts loaded through Expo don’t currently support the fontWeight or fontStyle properties—you
will need to load those variations of the font and specify them by name, as we have done here
with bold.
See also
A great article on async/await can be found at https://ponyfoo.com/articles/understandin
g-javascript-async-await.
Using font icons
Icons are an indispensable part of almost any app, particularly in navigation and
buttons. Similar to Expo's font helper, covered in the previous chapter, Expo also
has an icon helper that makes adding icon fonts much less of a hassle than using
vanilla React Native. In this recipe, we'll see how to use the icon helper module
with the popular FontAwesome and Ionicons icon font libraries.
Getting ready
We'll need to make a new project for this recipe. Let's name this project font-
icons.
How to do it...
1. We'll begin by opening App.js and importing the dependencies that we need
to build the app:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { FontAwesome, Ionicons } from '@expo/vector-icons';
2. Next, we can add the shell of the application, where we will display the
icons:
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
</View>
);
}
}
3. Inside of the View element, let's add two more View elements for holding icons
from each icon set:
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<View style={styles.iconRow}>
</View>
<View style={styles.iconRow}>
</View>
</View>
);
}
}
4. Now, let's add the styles for each of our declared elements. As we've seen in
previous recipes, the container styles fill the screen with flex: 1 and center
the items with alignItems and justifyContent set to center. The iconRow property
sets the flexDirection to row so that our icons will be lined up in a row:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
iconRow: {
flexDirection: 'row',
},
});
5. Now that the basic structure of our app is in place, let's add our icons. In the
first row of icons, we'll use four FontAwesome components to display four icons
from the FontAwesome font library. The name property determines which icon
should be used, the size property sets the size of the icon in pixels, and the
color sets what color the icon should be:
<View style={styles.iconRow}>
<FontAwesome style={styles.iconPadding} name="glass" size={48} color="green" />
<FontAwesome style={styles.iconPadding} name="beer" size={48} color="red" />
<FontAwesome style={styles.iconPadding} name="music" size={48} color="blue" />
<FontAwesome style={styles.iconPadding} name="taxi" size={48} color="#1CB5AD" />
</View>
Just as in CSS, the color property can be a color keyword defined in the CSS specification (you
can check out the full list in the MDN docs at https://developer.mozilla.org/en-
US/docs/Web/CSS/color_value), or a hex code for a given color.
6. In the next icon row View element, we'll add icons from the Ionicons font
library. As you can see, the Ionicons element takes the same properties as the
FontAwesome elements used in the previous step:
<View style={styles.iconRow}>
<Ionicons style={styles.iconPadding} name="md-pizza" size={48} color="orange" />
<Ionicons style={styles.iconPadding} name="md-tennisball" size={48} color="maroon" />
<Ionicons style={styles.iconPadding} name="ios-thunderstorm-outline" size={48} color="purple" />
<Ionicons style={styles.iconPadding} name="ios-happy-outline" size={48} color="#DF7977" />
</View>
7. The last step in this recipe is to add the remaining style, iconPadding, which
just adds some padding to evenly space out each of our icons:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
iconRow: {
flexDirection: 'row',
},
iconPadding: {
padding: 8,
}
});
8. That's all it takes! When we check out our app, there will be two rows of
icons, each row showcasing icons from FontAwesome and Ionicons respectively:
How it works...
The vector-icons package that comes with Expo provides access to 11 full icon
sets. All you have to do is import the associated component (for example, the
FontAwesome component for Font Awesome icons) and provide it with the name that
corresponds to the icon in the set that you'd like to use. You can find a full,
searchable list of all the icons you can use with the vector-icons helper library in
the vector-icons directory, hosted at https://expo.github.io/vector-icons/. Simply set
the element's name property to the icon name listed in the directory, add size and
color properties, and you're done!
As the GitHub README for vector-icons states, this library is a compatibility
layer created for using the icons provided by the react-native-vector-icons package
in Expo. You can find this package at https://github.com/oblador/react-native-vector-i
cons. If you are building a React Native app without Expo, you can get the same
functionality by using the react-native-vector-icons library instead.
See also
A catalog of all of the icons available in the vector-icons library can be found at ht
tps://expo.github.io/vector-icons/.
Implementing Complex User
Interfaces - Part II
This chapter will cover more recipes on building UIs with React Native. We'll
get our first look at linking to other applications and websites, handling a change
in device orientation, and how to build a form for collecting user input.
In this chapter, we will cover the following recipes:
Dealing with universal applications
Detecting orientation changes
Using a WebView to embed external websites
Linking to websites and other applications
Creating a form component
Dealing with universal applications
One of the benefits of using React Native is its ability to easily
create universal applications. We can share a lot of code between phone and
tablet applications. The layouts might change, depending on the device, but we
can reuse pieces of code for both types of device across layouts.
In this recipe, we will build an app that runs on phones and tablets. The tablet
version will include a different layout, but we will reuse the same internal
components.
Getting ready
For this recipe, we will show a list of contacts. For now, we will load the data
from a .json file. We will explore how to load remote data from a
Representational State Transfer (REST) API in a later chapter.
Let's open the following URL and copy the generated JSON to a file
called data.json at the root of the project. We will use this data to render the list of
contacts. It returns a JSON object of fake user data at http://api.randomuser.me/?resu
lts=20.
Let's create a new app called universal-app.
How to do it...
1. Let's open App.js and import the dependencies we'll need in this app, as well
as our data.json file we created in the previous Getting ready section. We'll
also import a Device utility from ./utils/Device, which we will build in a later
step:
import React, { Component } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import Device from './utils/Device';
import data from './data.json';
2. Here, we're going to create the main App component and its basic layout.
This top-level component will decide whether to render the phone or tablet
UI. We are only rendering two Text elements. The renderDetail text should be
displayed on tablets only and the renderMaster text should be displayed on
phones and tablets:
export default class App extends Component {
renderMaster() {
return (
<Text>Render on phone and tablets!!</Text>
);
}
renderDetail() {
if (Device.isTablet()) {
return (
<Text>Render on tablets only!!</Text>
);
}
}
render() {
return (
<View style={styles.content}>
{this.renderMaster()}
{this.renderDetail()}
</View>
);
}
}
3. Under the App component, we'll add a few basic styles. The styles
temporarily include paddingTop: 40 so that our rendered text is not overlapped
by the device's system bar:
const styles = StyleSheet.create({
content: {
paddingTop: 40,
flex: 1,
flexDirection: 'row',
},
});
4. If we try to run our app as it is, it will fail with an error telling us that
the Device module cannot be found, so let's create it. The purpose of this
utility class is to calculate whether the current device is a phone or tablet,
based on the screen dimensions. It will have an isTablet method and
an isPhone method. We need to create a utils folder in the root of the project
and add a Device.js for the utility. Now we can add the basic structure of the
utility:
import { Dimensions, Alert } from 'react-native';
// Tablet portrait dimensions
const tablet = {
width: 552,
height: 960,
};
class Device {
// Added in next steps
}
const device = new Device();
export default device;
5. Let's start building out the utility by creating two methods: one to get the
dimensions in portrait and the other to get the dimensions in landscape.
Depending on the device rotation, the values of width and height will change,
which is why we need these two methods to always get the correct values,
whether the device is landscape or portrait:
class Device {
getPortraitDimensions() {
const { width, height } = Dimensions.get("window");
return {
width: Math.min(width, height),
height: Math.max(width, height),
};
}
getLandscapeDimensions() {
const { width, height } = Dimensions.get("window");
return {
width: Math.max(width, height),
height: Math.min(width, height),
};
}
}
6. Now let's create the two methods our app will use to determine whether the
app is running on a tablet or a phone. To calculate this, we need to get the
dimensions in portrait mode and compare them with the dimensions we
have defined for a tablet:
isPhone() {
const dimension = this.getPortraitDimensions();
return dimension.height < tablet.height;
}
isTablet() {
const dimension = this.getPortraitDimensions();
return dimension.height >= tablet.height;
}
7. Now, if we open the app, we should see two different texts being rendered,
depending on whether we're running the app on a phone or a tablet:
8. The utility works as expected! Let's return to working on the renderMaster
method of the main App.js. We want this method to render the list of
contacts that live in the data.json file. Let's import a new component, which
we'll build out in the following steps, and update the renderMaster method to
use our new component:
import UserList from './UserList';
export default class App extends Component {
renderMaster() {
return (
<UserList contacts={data.results} />
);
}
//...
}
9. Let's create a new UserList folder. Inside this folder, we need to create
the index.js and styles.js files for the new component. The first thing we
need to do is import the dependencies into the new index.js, create the
UserList class, and export it as the default:
import React, { Component } from 'react';
import {
StyleSheet,
View,
Text,
ListView,
Image,
TouchableOpacity,
} from 'react-native';
import styles from './styles';
export default class UserList extends Component {
// Defined in the following steps
}
10. We've already covered how to create a list. If you are not clear on how the
ListView component works, read the Displaying a list of items recipe in Chapte
r 2, Creating a Simple React Native App. In the constructor of the class, we
will create the dataSource and then add it to the state:
export default class UserList extends Component {
constructor(properties) {
super(properties);
const dataSource = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.state = {
dataSource: dataSource.cloneWithRows(properties.contacts),
};
}
//...
}
11. The render method also follows the same pattern introduced in the ListView
recipe, Displaying a list of items, from Chapter 2, Creating a Simple React
Native App:
render() {
return (
<View style={styles.main}>
<Text style={styles.toolbar}>
My contacts!
</Text>
<ListView dataSource={this.state.dataSource}
renderRow={this.renderContact}
style={styles.main} />
</View> );
}
12. As you can see, we need to define the renderContact method to render each of
the rows. We are using the TouchableOpacity component as the main wrapper,
which will allow us to use a callback function to perform some actions
when a list item is pressed. For now, we are not doing anything when the
button is pressed. We will learn more about communicating between
components using Redux in future chapters:
renderContact = (contact) => {
return (
<TouchableOpacity style={styles.row}>
<Image source={{uri: `${contact.picture.large}`}} style=
{styles.img} />
<View style={styles.info}>
<Text style={styles.name}>
{this.capitalize(contact.name.first)}
{this.capitalize(contact.name.last)}
</Text>
<Text style={styles.phone}>{contact.phone}</Text>
</View>
</TouchableOpacity>
);
}
13. We don't have a way to capitalize the texts using styles, so we need to use
JavaScript for that. The capitalize function is quite simple, and sets the first
letter of the given string to uppercase:
capitalize(value) {
return value[0].toUpperCase() + value.substring(1);
}
14. We are almost done with this component. All that's left are the styles. Let's
open the /UserList/styles.js file and add styles for the main container and
the toolbar:
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
main: {
flex: 1,
backgroundColor: '#dde6e9',
},
toolbar: {
backgroundColor: '#2989dd',
color: '#fff',
paddingTop: 50,
padding: 20,
textAlign: 'center',
fontSize: 20,
},
// Remaining styles added in next step.
});
15. Now, for each row, we want to render the image of each contact on the left,
and the contact's name and phone number on the right:
row: {
flexDirection: 'row',
padding: 10,
},
img: {
width: 70,
height: 70,
borderRadius: 35,
},
info: {
marginLeft: 10,
},
name: {
color: '#333',
fontSize: 22,
fontWeight: 'bold',
},
phone: {
color: '#aaa',
fontSize: 16,
},
16. Let's switch over to the App.js file and remove the paddingTop property we
used for making text legible in step 7; the line to be removed is shown in
bold:
const styles = StyleSheet.create({
content: {
paddingTop: 40,
flex: 1,
flexDirection: 'row',
},
});
17. If we try to run our app, we should be able to see a really nice list on the
phone as well as the tablet, and the same component on the two different
devices:
18. We are already displaying two different layouts based on the current device!
Now we need to work on the UserDetail view, which will show the selected
contact. Let's open App.js, import the UserDetail views, and update
the renderDetail method, as follows:
import UserDetail from './UserDetail';
export default class App extends Component {
renderMaster() {
return (
<UserList contacts={data.results} />
);
}
renderDetail() {
if (Device.isTablet()) {
return (
<UserDetail contact={data.results[0]} />
);
}
}
}
As mentioned earlier, in this recipe, we are not focusing on sending data from one component
to another, but instead on rendering a different layout in tablets and phones. Therefore, we
will always send the first record to the user details view for this recipe.
19. To make things simple and to make the recipe as short as possible, for the
user details view, we will only display a toolbar and some text showing the
first and last name of the given record. We are going to use a stateless
component here:
import React from 'react';
import {
View,
Text,
} from 'react-native';
import styles from './styles';
const UserList = ({ contact }) => (
<View style={styles.main}>
<Text style={styles.toolbar}>Details should go here!</Text>
<Text>
This is the detail view:{contact.name.first} {contact.name.last}
</Text>
</View>
);
export default UserList;
20. Finally, we need to style this component. We want to assign three-quarters
of the screen to the details page and one-quarter to the master list. This can
be done easily by using flexbox. Since the UserList component has a flex
property of 1, we can set the flex property of UserDetail to 3, allowing
UserDetail to take up 75% of the screen. Here are the styles we'll add to
the /UserDetail/styles.js file:
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
main: {
flex: 3,
backgroundColor: '#f0f3f4',
},
toolbar: {
backgroundColor: '#2989dd',
color: '#fff',
paddingTop: 50,
padding: 20,
textAlign: 'center',
fontSize: 20,
},
});
export default styles;
21. If we try to run our app again, we will see that on the tablet, it will render a
nice layout showing both the list view and the detail view, while on the
phone, it only shows the list of contacts:
How it works...
In the Device utility, we imported a dependency that React Native provides called
Dimension for getting the dimensions of the current device. This is what we need in
order to figure out whether our app is running on a phone or a tablet.
We also defined a tablet constant in the Device utility, which is an object
containing the width and height that is used to calculate whether the device is a
tablet or not. The values of this constant are based on the smallest Android tablet
available on the market.
In step 5, we got the width and height by calling
the Dimensions.get("window") method, and then we got the maximum and minimum
values depending on the orientation we wanted.
In step 12, it's important to note that we used an arrow function to define
the renderContact method. Using an arrow function keeps the correct binding
scope, otherwise, the this in the call to this.capitalize would be bound to the
wrong scope. Check the See also section for more information on how both the
this keyword and arrow functions work.
See also
A good explanation of ES6 arrow functions from ponyfoo at https://ponyfoo.c
om/articles/es6-arrow-functions-in-depth
An in-depth look at how this works in JavaScript by Kyle Simpson at https:
//github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch
2.md
Detecting orientation changes
When building complex interfaces, it's very common to render different UI
components, based on the device's orientation. This is especially
true when dealing with tablets.
In this recipe, we will render a menu based on screen orientation. In landscape,
we will render an expanded menu with icons and texts, and in portrait, we will
only render the icons.
Getting ready
To support orientation changes, we are going to use Expo's helper utility called
ScreenOrientation.
We will also use the FontAwesome component provided by the Expo
package @expo/vector-icons. The Using font icons recipe in Chapter 2, Creating a
Simple React Native App, describes how to use this component.
Before we get started, let's create a new app called screen-orientation. We'll also
need to make a tweak to the app.json file that Expo creates in the root of the
directory. This file has a few basic settings Expo uses when building the app.
One of these settings is orientation, which is automatically set to portrait for every
new app. This setting determines the orientations the app allows, and can be set
to portrait, landscape, or default. If we change this to default, our app will allow
both portrait and landscape orientations.
To see these changes take effect, be sure to restart your Expo project.
How to do it...
1. We'll start by opening App.js and adding the imports we'll be using:
import React from 'react';
import {
Dimensions,
StyleSheet,
Text,
View
} from 'react-native';
2. Next, we'll add the empty App class for the component, along with some
basic styles:
export default class App extends React.Component {
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff'
},
text: {
fontSize: 40,
}
});
3. With the shell of our app in place, we can now add the render method. In the
render method, you'll notice we've got a View component using the onLayout
property, which will fire off whenever the orientation of the device changes.
The onLayout will then run this.handleLayoutChange, which we will define in the
next step. In the Text element, we simply display the value of orientation on
the state object:
export default class App extends React.Component {
render() {
return (
<View
onLayout={() => this.handleLayoutChange}
style={styles.container}
>
<Text style={styles.text}>
{this.state.orientation}
</Text>
</View>
);
}
}
4. Let's create the handleLayoutChange method of our component, as well as the
getOrientation function that the handleLayoutChange method calls.
The getOrientation function uses the React Native Dimensions utility to get the
width and height of the screen. If height > width, we know that the device is
in portrait orientation, and if not, that it is in landscape orientation. By
updating state, a re-render will be initiated, and the value of
this.state.orientation will reflect the orientation:
handleLayoutChange() {
this.getOrientation();
}
getOrientation() {
const { width, height } = Dimensions.get('window');
const orientation = height > width ? 'Portrait' : 'Landscape';
this.setState({
orientation
});
}
5. If we run the app at this point, we'll get the error TypeError: null is not an
object: (evaluating 'this.state.orientation'). This happens because the render
method is attempting to read from the this.state.orientation value before it's
even been defined. We can easily fix this problem by getting the orientation
before render runs for the first time, via the React life cycle
componentWillMount hook:
componentWillMount() {
this.getOrientation();
}
6. That's all it takes to get the basic functionality we're looking for! Run the
app again and you should see the displayed text reflect the orientation of the
device. Rotate the device, and the orientation text should update:
7. Now that the orientation state value is updating properly, we can focus on
the UI. As mentioned before, we will create a menu that renders the options
slightly differently based on the current orientation. Let's import a Menu
component, which we'll build out in the next steps, and update
the render method of our App component to use the new Menu component.
Notice that we are now passing this.state.orientation to the orientation
property of the Menu component:
import Menu from './Menu';
export default class App extends React.Component {
// ...
render() {
return (
<View
onLayout={() => {this.handleLayoutChange()}}
style={styles.container}
>
<Menu orientation={this.state.orientation} />
<View style={styles.main}>
<Text>Main Content</Text>
</View>
</View>
);
}
}
8. Let's also update the styles for our App component. You can replace the
styles from step 2 with the following code. By setting the flexDirection
to row on the container styles, we'll be able to display the two components
horizontally:
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
},
main: {
flex: 1,
backgroundColor: '#ecf0f1',
justifyContent: 'center',
alignItems: 'center',
}
});
9. Next, let's build out the Menu component. We'll need to create a
new /Menu/index.js file, which will define the Menu class. This
component will receive the orientation property and decide how to render
the menu options based on the orientation value. Let's start by importing the
dependencies for this class:
import React, { Component } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { FontAwesome } from '@expo/vector-icons';
10. Now we can define the Menu class. On the state object, we will define an
array of options. These option objects will be used to define the icons. As
discussed in the Using font icons recipe in the previous chapter, we can
define icons via keywords, as defined in the vector-icon directory, found at
https://expo.github.io/vector-icons/:
export default class Menu extends Component {
state = {
options: [
{title: 'Dashboard', icon: 'dashboard'},
{title: 'Inbox', icon: 'inbox'},
{title: 'Graphs', icon: 'pie-chart'},
{title: 'Search', icon: 'search'},
{title: 'Settings', icon: 'gear'},
],
};
// Remainder defined in following steps
}
11. The render method for this component loops through the array of options in
the state object:
render() {
return (
<View style={styles.content}>
{this.state.options.map(this.renderOption)}
</View>
);
}
12. As you can see, inside the JavaScript XML (JSX) in the last step, there's a
call to renderOption. In this method, we are going to render the icon and the
label for each option. We'll also use the orientation value to toggle showing
the label, and to change the icon's size:
renderOption = (option, index) => {
const isLandscape = this.properties.orientation === 'Landscape';
const title = isLandscape
? <Text style={styles.title}>{option.title}</Text>
: null;
const iconSize = isLandscape ? 27 : 35;
return (
<View key={index} style={[styles.option, styles.landscape]}>
<FontAwesome name={option.icon} size={iconSize} color="#fff" />
{title}
</View>
);
}
In the previous code block, notice that we are defining a key property. When dynamically
creating a new component, we always need to set a key property. This property should be
unique for each item, since it's used internally by React. In this case, we are using the index of
the loop iteration. This way, we can be assured that every item will have a unique key value.
You can read more about it in the official documentation: https://reactjs.org/docs/lists-and-keys.html.
13. Finally, we'll define the styles for the menu. First, we will set
the backgroundColor to dark blue, and then, for each option, we'll change
the flexDirection to render the icon and label horizontally. The rest of the
styles add margins and paddings so that the menu items are nicely spaced
apart:
const styles = StyleSheet.create({
content: {
backgroundColor: '#34495e',
paddingTop: 50,
},
option: {
flexDirection: 'row',
paddingBottom: 15,
},
landscape: {
paddingRight: 30,
paddingLeft: 30,
},
title: {
color: '#fff',
fontSize: 16,
margin: 5,
marginLeft: 20,
},
});
14. If we run our application now, it will display the menu UI differently
depending on the orientation of the screen. Rotate the device, and the layout
will automatically update:
There's more...
In this recipe, we had our first look at the app.json file that exists as part of every
Expo project. There are many useful settings that can be adjusted in this file that
affect the build process of the project. You can use this file to adjust orientation
lock, define an app icon, and set a splash screen, among many other settings.
You can review all of the settings supported by app.json in the Expo configuration
documentation, hosted at https://docs.expo.io/versions/latest/guides/configuration.htm
l.
Expo also provides the ScreenOrientation utility, which can be used instead to
declare the allowed orientations for your app. Using the utility's main method
ScreenOrientation.allow(orientation), will overwrite the corresponding setting in
app.json. The utility also provides more granular options than the setting in
app.json, such as ALL_BUT_UPSIDE_DOWN and LANDSCAPE_RIGHT. For more on this utility, you
can read the documentation at https://docs.expo.io/versions/latest/sdk/screen-orientat
ion.html.
Using a WebView to embed external
websites
For many applications, it's required that external links can be visited and
displayed within the app. This can be for showing a third-party website, online
help, and the terms and conditions of using your app, among other things.
In this recipe, we will see how to open a WebView by clicking on a button in our
app and dynamically setting the URL value. We'll also be using the react-
navigation package for to create basic stack navigation in this recipe. Please check
out the Setting up and using navigation recipe in Chapter 3, Implementing
Complex User Interfaces – Part I for a deeper dive into building navigation.
If the needs of your app are better met by loading external websites via the
device's browser, see the next recipe, Linking to websites and other applications.
Getting ready
We will need to create a new app for our WebView-based recipe. Let's name our
new app web-view. We'll also be using react-navigation, so be sure to install this as
well. You can use yarn or npm to install the package. In the root of the project, run
the following:
yarn add react-navigation
Alternatively, install them using npm:
npm install --save react-navigation
How to do it...
1. Let's start by opening the App.js file. In this file, we'll be using the
StackNavigator component provided by the react-navigation package. First, let's
add the imports we'll be using in this file. HomeScreen is a component we will
be building later in this recipe:
import React, { Component } from 'react';
import { StackNavigator } from 'react-navigation';
import HomeScreen from './HomeScreen';
2. Now that we have our imports, let's use the StackNavigator component to
define the first route; we'll be using a Home route with links that should be
displayed using the React Native WebView component. The navigationOptions
property allows us to define a title to be displayed in the navigation header:
const App = StackNavigator({
Home: {
screen: HomeScreen,
navigationOptions: ({ navigation }) => ({
title: 'Home'
}),
},
});
export default App;
3. We are now ready to create the HomeScreen component. Let's create a new
folder in the root of our project, called HomeScreen and add an index.js file to
the folder. As usual, we can begin with our imports:
import React, { Component } from 'react';
import {
TouchableOpacity,
View,
Text,
SafeAreaView,
} from 'react-native';
import styles from './styles';
4. Now we can declare our HomeScreen component. Let's also add a state object
to the component with a links array. This array has an object for each link
we'll be using in this component. I've provided four links for you to use;
however, you can edit the title and url in each links array object to any
websites you'd like:
export default class HomeScreen extends Component {
state = {
links: [
{
title: 'Smashing Magazine',
url: 'https://www.smashingmagazine.com/articles/'
},
{
title: 'CSS Tricks',
url: 'https://css-tricks.com/'
},
{
title: 'Gitconnected Blog',
url: 'https://medium.com/gitconnected'
},
{
title: 'Hacker News',
url: 'https://news.ycombinator.com/'
}
],
};
}
5. We're ready to add a render function to this component. Here, we are using
the SafeAreaView for the container element. This works just like a normal View
element, but also accounts for the notch area on the iPhone X so that no part
of our layout is obscured by the device bezels. You'll notice that we are
using map to map over the links array from the previous step, passing each
one to the renderButton function:
render() {
return (
<SafeAreaView style={styles.container}>
<View style={styles.buttonList}>
{this.state.links.map(this.renderButton)}
</View>
</SafeAreaView>
);
}
6. Now that we have defined the render method, we'll need to create the
renderButton method that it's using. This method takes each link as a
parameter called button, and the index, which we'll use as the unique key for
each element renderButton is creating. For more on this point, see the Tip in
step 12 of the second recipe in this chapter, Detecting orientation changes.
The TouchableOpacity button element will fire this.handleButtonPress(button)
when pressed:
renderButton = (button, index) => {
return (
<TouchableOpacity
key={index}
onPress={() => this.handleButtonPress(button)}
style={styles.button}
>
<Text style={styles.text}>{button.title}</Text>
</TouchableOpacity>
);
}
7. Now we need to create the handleButtonPress method used in the previous
step. This method uses the url and title properties from the passed-in button
parameter. We can then use these in a call to
this.properties.navigation.navigate(), passing in the name of the route we want
to navigate to and the parameters that should be passed along to that route.
We have access to a property called navigation because we are using
StackNavigator, which we set up in step 2:
handleButtonPress(button) {
const { url, title } = button;
this.properties.navigation.navigate('Browser', { url, title });
}
8. The HomeScreen component is done, except for the styles. Let's add a styles.js
file in the HomeScreen folder to define these styles:
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
buttonList: {
flex: 1,
justifyContent: 'center',
},
button: {
margin: 10,
backgroundColor: '#c0392b',
borderRadius: 3,
padding: 10,
paddingRight: 30,
paddingLeft: 30,
},
text: {
color: '#fff',
textAlign: 'center',
},
});
export default styles;
9. Now, if we open the app, we should see the HomeScreen component being
rendered with our list of four link buttons, and a header with the title Home
rendered in the native style on each device. Since there is no Browser route in
our StackNavigator, however, the buttons will not actually do anything when
pressed:
10. Let's return to the App.js file and add the Browser route. First, we'll need to
import the BrowserScreen component, which we'll create in the following
steps:
import BrowserScreen from './BrowserScreen';
11. Now that the BrowserScreen component has been imported, we can add it to
the StackNavigator object to create a Browser route. In navigationOptions, we're
defining a dynamic title based on the parameters passed to the route. These
parameters are the same as the object we passed into the
navigation.navigate() call as the second argument in step 7:
const App = StackNavigator({
Home: {
screen: HomeScreen,
navigationOptions: ({ navigation }) => ({
title: 'Home'
}),
},
Browser: {
screen: BrowserScreen,
navigationOptions: ({ navigation }) => ({
title: navigation.state.params.title
}),
},
});
12. We are ready to create the BrowserScreen component. Let's create a new folder
in the root of the project called BrowserScreen with a new index.js file inside,
then add the imports this component needs:
import React, { Component } from 'react';
import { WebView } from 'react-native';
13. The BrowserScreen component is fairly simple. It consists only of a render
method that reads the params property from the navigation.state property
passed in to call to the this.properties.navigation.navigate that fires when a
button is pressed, as defined in step 7. All we need to do is render the
WebView component and set its source property to an object with the uri
property set to params.url:
export default class BrowserScreen extends Component {
render() {
const { params } = this.properties.navigation.state;
return(
<WebView
source={{uri: params.url}}
/>
);
}
}
14. Now, if we go back to the app running in the simulator, we can see our
WebView in action!
Hacker News and Smashing Magazine visited from our app
How it works...
Using a WebView to open external sites is a great way to allow a user to
consume external websites while keeping them in our app. Many applications
out there do this, allowing the user to return to the main portion of the app easily.
In step 6, we used an arrow function to bind the function in the onPress property
to the scope of the current class instance, since we are using this function when
looping through the array of links.
In step 7, whenever a button is pressed, we use the title and URL that are bound
to that button, passing them along as parameters as we navigate to the Browser
screen. The navigationOptions in step 11 use this same title value as the title of the
screen. The navigationOptions take a function whose first parameter is an object
containing navigation, which provides the parameters used when navigating. In
step 11, we structure navigation from this object so that we can set the view's
title to navigation.state.params.title.
Thanks to the StackNavigator component provided by react-navigation, we get a
header with OS-specific animations, built in with a back button. You can read
the StackNavigation documentation for more information on this component at https
://reactnavigation.org/docs/stack-navigator.html.
Step 13 uses the URL passed to the BrowserScreen component to render a WebView
by using the URL in the WebView's source property. You can find a list of all
available WebView properties in the official documentation located at https://face
book.github.io/react-native/docs/webview.html.
Linking to websites and other
applications
We have learned how to use a WebView to render a third-party website as an
embedded part of our app. However, sometimes, we might want to use the native
browser to open a site, link to other native system applications (such as email,
phone, and SMS), or even deep link to a completely separate app.
In this recipe, we will link to an external site via both the native browser and a
browser modal within our app, create links to the phone and messaging
applications, and create a deep link that will open the Slack app and
automatically load the #general channel in the gitconnected.com Slack group.
You will need to run this app on a real device in order to open the links in this app that use the
device's system applications, such as email, phone, and SMS links. In my experience, this will
not work in the simulator.
Getting ready
Let's create a new app for this recipe. We'll call it linking-app.
How to do it...
1. Let's start by opening App.js and adding the imports we'll be using:
import React from 'react';
import { StyleSheet, Text, View, TouchableOpacity, Platform } from 'react-native';
import { Linking } from 'react-native';
import { WebBrowser } from 'expo';
2. Next, let's add both an App component and a state object. In this app, the
state object will house all of the links that we'll be using in this recipe in an
array called links. Notice how the url property in each links object has a
protocol attached to it (tel, mailto, sms, and so on). These protocols are used
by the device to properly handle each link:
export default class App extends React.Component {
state = {
links: [
{
title: 'Call Support',
url: 'tel:+12025550170',
type: 'phone'
},
{
title: 'Email Support',
url: 'mailto:support@email.com',
type: 'email',
},
{
title: 'Text Support',
url: 'sms:+12025550170',
type: 'text message',
},
{
title: 'Join us on Slack',
url: 'slack://channel?team=T5KFMSASF&id=C5K142J57',
type: 'slack deep link',
},
{
title: 'Visit Site (internal)',
url: 'https://google.com',
type: 'internal link'
},
{
title: 'Visit Site (external)',
url: 'https://google.com',
type: 'external link'
}
]
}
}
The phone number used in the Text Support and Call Support buttons is an unused number at
the time of writing, as generated by https://fakenumber.org/. This number is likely to still be
unused, but this could possibly change. Feel free to use a different fake number for these links,
just make sure to keep the protocol in place.
3. Next, let's add the render function for our app. The JSX here is simple: we
map over the state.links array from the previous step, passing each to our
renderButton function defined in the next step:
render() {
return(
<View style={styles.container}>
<View style={styles.buttonList}>
{this.state.links.map(this.renderButton)}
</View>
</View>
);
}
4. Let's build out the renderButton method used in the last step. For each link,
we create a button with TouchableOpacity and set the onPress property to
execute the handleButtonPress and pass it the button property:
renderButton = (button, index) => {
return(
<TouchableOpacity
key={index}
onPress={() => this.handleButtonPress(button)}
style={styles.button}
>
<Text style={styles.text}>{button.title}</Text>
</TouchableOpacity>
);
}
5. Next, we can build out the handleButtonPress function. Here, we'll be using the
type property that we've added to each object in the links array. If the type
is 'internal link', we want to open the URL within our app using the
Expo WebBrowser component's openBrowserAsync method, and for everything else,
we'll use the React Native Linking component's openURL method.
If there's a problem with the openURL call and the URL is using the slack://
protocol, it means the device does not know how to handle the protocol,
probably because the slack app isn't installed. We'll handle this problem
with the handleMissingApp function, which we'll add in the next step:
handleButtonPress(button) {
if (button.type === 'internal link') {
WebBrowser.openBrowserAsync(button.url);
} else {
Linking.openURL(button.url).catch(({ message }) => {
if (message.includes('slack://')) {
this.handleMissingApp();
}
});
}
}
6. Now we can create our handleMissingApp function. Here, we use the React
Native helper Platform, which provides information about the platform the
app is running on. Platform.OS will always return the operating system,
which, on phones, should always resolve to either 'ios' or 'android'. You can
read more about the capabilities of Platform in the official documentation at h
ttps://facebook.github.io/react-native/docs/platform-specific-code.html.
If the link to the Slack app does not work as expected, we'll use
Linking.openURL again; this time, to open the app in the app store appropriate
for the device:
handleMissingApp() {
if (Platform.OS === 'ios') {
Linking.openURL(`https://itunes.apple.com/us/app/id618783545`);
} else {
Linking.openURL(
`https://play.google.com/store/applications/details?id=com.Slack`
);
}
}
7. Our app doesn't have any styles yet, so let's add some. Nothing fancy here,
just aligning the buttons in the center of the screen, coloring and centering
text, and providing padding on each button:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
justifyContent: 'center',
alignItems: 'center',
},
buttonList: {
flex: 1,
justifyContent: 'center',
},
button: {
margin: 10,
backgroundColor: '#c0392b',
borderRadius: 3,
padding: 10,
paddingRight: 30,
paddingLeft: 30,
},
text: {
color: '#fff',
textAlign: 'center',
},
});
8. That's all there is to this app. Once we load the app, there should be a
column of buttons representing each of our links. Note that some of these
links will only behave properly on an actual device. I recommend running
this recipe on a real device to see whether all the links work properly:
How it works...
In step 2, we defined all the links that our app uses. Each link object has a type
property that we use in the handleButtonPress method defined in step 5.
This handleButtonPress function uses the link's type to determine which one of two
strategies will be used. If the link's type is 'internal link', we want to open the
link with the device browser as a modal that pops up within the app itself. For
this purpose, we can use Expo's WebBrowser helper, passing the URL to its
openBrowserAsync method. If the link's type is 'external link', we'll open the link with
React Native's Linking helper. This lets you see the different ways you can open a
website from your app.
The Linking helper can handle protocols other than HTTP and HTTPS as well. By
simply using the proper protocol in the link we pass to Linking.openURL, we can
open the telephone (tel:), messaging (sms:), or email (mailto:).
Linking.openURL can also handle deep links to other applications, as long as the app
you want to link to has a protocol for doing so, such as how we open Slack by
using the slack:// protocol. For more information on Slack's deep linking
protocol and what you can do with it, visit their documentation at https://api.slac
k.com/docs/deep-linking.
In step 5, we catch any error caused by calling Linking.openURL, check whether the
error was caused by the Slack protocol using message.includes('slack://'), and if so,
we know the Slack app is not installed on the device. In this case, we fire
handleMissingApp, which opens the app store link for Slack using the appropriate
link, as determined by Platform.OS.
See also
Official documentation on the Linking module can be found at https://docs.expo.io/
versions/latest/guides/linking.html.
Creating a form component
Most applications require a way to input data, whether it's a simple registration
and login form or a more complex component with many input fields and
controls.
In this recipe, we will create a form component to handle text inputs. We will
collect data using different keyboards, and show an alert message with the
resulting information.
Getting ready
We need to create an empty app. Let's name it user-form.
How to do it...
1. Let's start by opening App.js and adding our imports. The imports include
the UserForm component that we'll be building out in a later step:
import React from 'react';
import {
Alert,
StyleSheet,
ScrollView,
SafeAreaView,
Text,
TextInput,
} from 'react-native';
import UserForm from './UserForm';
2. Since this component is going to be very simple, we are going to create a
stateless component for our App. We will only render a top toolbar inside
a ScrollView for the UserForm component:
const App = () => (
<SafeAreaView style={styles.main}>
<Text style={styles.toolbar}>Fitness App</Text>
<ScrollView style={styles.content}>
<UserForm />
</ScrollView>
</SafeAreaView>
);
const styles = StyleSheet.create({
// Defined in a later step
});
export default App;
3. We need to add some styles to these components. We'll add some colors and
padding, as well as setting the main class to flex: 1 to fill the remainder of the
screen:
const styles = StyleSheet.create({
main: {
flex: 1,
backgroundColor: '#ecf0f1',
},
toolbar: {
backgroundColor: '#1abc9c',
padding: 20,
color: '#fff',
fontSize: 20,
},
content: {
padding: 10,
},
});
4. We have defined the main App component. Now let's get to work on the
actual form. Let's create a new directory called UserForm in the base of the
project and add an index.js file. Then, we'll import all the dependencies for
this class:
import React, { Component } from 'react';
import {
Alert,
StyleSheet,
View,
Text,
TextInput,
TouchableOpacity,
} from 'react-native';
5. This is the class that will render the inputs and keep track of the data. We
are going to save the data on the state object, so we'll start by initializing
state as an empty object:
export default class UserForm extends Component {
state = {};
// Defined in a later step
}
const styles = StyleSheet.create({
// Defined in a later step
});
6. In the render method, we are going to define the components that we want to
display, which in this case are three text inputs and a button. We are going
to define a renderTextfield method that accepts a configuration object as a
parameter. We'll define the name of the field, the placeholder, and
the keyboard type that should be used on the input. In addition, we're also
calling a renderButton method that will render the Save button:
render() {
return (
<View style={styles.panel}>
<Text style={styles.instructions}>
Please enter your contact information
</Text>
{this.renderTextfield({ name: 'name', placeholder: 'Your
name' })}
{this.renderTextfield({ name: 'phone', placeholder: 'Your
phone number', keyboard: 'phone-pad' })}
{this.renderTextfield({ name: 'email', placeholder: 'Your
email address', keyboard: 'email-address'})}
{this.renderButton()}
</View>
);
}
7. To render the text fields, we are going to use the TextInput component in our
renderTextfield method. This TextInput component is provided by React
Native and works on both iOS and Android. The keyboardType property
allows us to set the keyboard that we want to use. The four available
keyboards on both platforms are default, numeric, email-address, and phone-pad:
renderTextfield(options) {
return (
<TextInput
style={styles.textfield}
onChangeText={(value) => this.setState({ [options.name]:
value })}
placeholder={options.label}
value={this.state[options.name]}
keyboardType={options.keyboard || 'default'}
/>
);
}
8. We already know how to render buttons and respond to the Press action. If
this is unclear, I recommend reading the Creating a reusable button with
theme support recipe in Chapter 3, Implementing Complex User Interfaces –
Part I:
renderButton() {
return (
<TouchableOpacity
onPress={this.handleButtonPress}
style={styles.button}
>
<Text style={styles.buttonText}>Save</Text>
</TouchableOpacity>
);
}
9. We need to define the onPressButton callback. For simplicity, we'll just show
an alert with the input data that we have on the state object:
handleButtonPress = () => {
const { name, phone, email } = this.state;
Alert.alert(`User's data`,`Name: ${name}, Phone: ${phone}, Email:
${email}`);
}
10. We are almost done with this recipe! All we need to do is apply some styles
– some colors, padding, and margins; nothing fancy really:
const styles = StyleSheet.create({
panel: {
backgroundColor: '#fff',
borderRadius: 3,
padding: 10,
marginBottom: 20,
},
instructions: {
color: '#bbb',
fontSize: 16,
marginTop: 15,
marginBottom: 10,
},
textfield: {
height: 40,
marginBottom: 10,
},
button: {
backgroundColor: '#34495e',
borderRadius: 3,
padding: 12,
flex: 1,
},
buttonText: {
textAlign: 'center',
color: '#fff',
fontSize: 16,
},
});
11. If we run our app, we should be able to see a form that uses native controls
on both Android and iOS, as expected:
You might not be able to see the keyboard as defined by keyboardType when running your app in
a simulator. Run the app on a real device to ensure that the keyboardType is properly changing
the keyboard for each TextInput.
How it works...
In step 8, we defined the TextInput component. In React (and React Native), we
can use two types of input: controlled and uncontrolled components. In this
recipe, we're using controlled input components, as recommended by the React
team.
A controlled component will have a value property, and the component will
always display the content of the value property. This means that we need a way
to change the value when the user starts typing the input. If we don't update that
value, then the text in the input won't ever change, even if the user tries to type
something.
In order to update the value, we can use the onChangeText callback and set the new
value. In this example, we are using the state to keep track of the data and we are
setting a new key on the state with the content of the input.
An uncontrolled component, on the other hand, will not have a value property
assigned. We can assign an initial value using the defaultValue property.
Uncontrolled components have their own state, and we can get their value by
using an onChangeText callback, just as we can with controlled components.
Implementing Complex User
Interfaces - Part III
In this chapter, we will cover the following recipes:
Creating a map app with Google Maps
Creating an audio player
Creating an image carousel
Adding push notifications to your app
Implementing browser-based authentication
Introduction
In this chapter, we'll cover some of the more advanced features you might need
to add to an app. The applications we'll build in this chapter include building a
fully functional audio player, Google Maps integration, and implementing
browser-based authentication so that your app can connect to public APIs for
developers.
Creating a map app with Google
Maps
Using a mobile device is a portable experience, so it's no surprise that maps are a
common part of many iOS and Android applications. Your app may need to tell
a user where they are, where they're going, or where other users are in real time.
In this recipe, we'll be making a simple app that uses Google Maps on Android,
and Apple's Maps app on iOS, to display a map centered on the user's location.
We will be using Expo's Location helper library to get the latitude and longitude of
the user and will use that data to render the map using Expo's MapView component.
MapView is an Expo ready version of the react-native-maps package created by
Airbnb, so you can expect the react-native-maps documentation to apply, which
can be found at https://github.com/react-community/react-native-maps.
Getting ready
We will need to create a new app for this recipe. Let's call it google-maps. Since the
user pin in this recipe will use a custom icon, we'll also need an image for that. I
used the icon You Are Here by Maico Amorim, which you can download from ht
tps://thenounproject.com/term/you-are-here/12314/. Feel free to use any image you'd
like to represent the user pin. Save the image to the assets folder in the root of the
project.
How to do it...
1. We'll start by opening App.js and adding our imports:
import React from 'react';
import {
Location,
Permissions,
MapView,
Marker
} from 'expo';
import {
StyleSheet,
Text,
View,
} from 'react-native';
2. Next, let's define the App class and the initial state. In this recipe, state will
only need to keep track of the user's location, which we initialize to null:
export default class App extends Component {
state = {
location: null
}
// Defined in following steps
}
3. Next, we'll define the componentDidMount life cycle hook, which will ask the
user to grant permission to access the user's location via the device's
geolocation. If the user grants the app permission to use its location, the
return object will have a status property with the value 'granted'. If granted,
we'll get the user's location with this.getLocation, defined in the next step:
async componentDidMount() {
const permission = await Permissions.askAsync(Permissions.LOCATION);
if (permission.status === 'granted') {
this.getLocation();
}
}
4. The getLocation function is simple. It grabs the location information from the
device's GPS using the getCurrentPositionAsync method of the Location
component, then saves that location information to state. That information
contains the latitude and longitude of the user, which we'll use when we
render the map:
async getLocation() {
let location = await Location.getCurrentPositionAsync({});
this.setState({
location
});
}
5. Now, let's use that location information to render our map. First, we'll check
that a location has been saved on state. If so, we'll render the MapView, and
otherwise render null. The only property we need to set to render our map is
the initialRegion property, which defines the location the map should display
when it is first rendered. We'll pass this property on the object with the
latitude and longitude saved to state, and define a starting zoom level
with latitudeDelta and longitudeDelta:
renderMap() {
return this.state.location ?
<MapView
style={styles.map}
initialRegion={{
latitude: this.state.location.coords.latitude,
longitude: this.state.location.coords.longitude,
latitudeDelta: 0.09,
longitudeDelta: 0.04,
}}
>
// Map marker is defined in next step
</MapView> : null
}
6. Within the MapView, we'll need to add a marker at the user's current location.
The Marker component is part of the MapView parent component, so in the JSX
we'll define a MapView.Marker child element of the MapView element. This
element takes the user's location, a title, and description for displaying
when the icon is tapped, and a custom image via the image property:
<MapView
style={styles.map}
initialRegion={{
latitude: this.state.location.coords.latitude,
longitude: this.state.location.coords.longitude,
latitudeDelta: 0.09,
longitudeDelta: 0.04,
}}
>
<MapView.Marker
coordinate={this.state.location.coords}
title={"User Location"}
description={"You are here!"}
image={require('./assets/you-are-here.png')}
/>
</MapView> : null
7. Now, let's define our render function. It simply renders the map within a
containing View element:
render() {
return (
<View style={styles.container}>
{this.renderMap()}
</View>
);
}
8. Lastly, let's add our styles. We'll set flex to 1 on both the container and the
map, so that both fill the screen:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
map: {
flex: 1
}
});
9. Now, if we open the app, we'll see a map rendered with our custom user
icon at the location provided by the device! Unfortunately, Google Maps
integration will not work in the Android emulator, so a real device will be
needed to test the Android implementation of the app. Don't be surprised
that the iOS app running on a simulator displays the user's location in San
Francisco; this is due to how Xcode location defaults work. Run it on a real
iOS device to see the render your location:
How it works...
By making use of the MapView component provided by Expo, the implementation
of a map in your React Native app is now a much simpler and straightforward
process than it once was.
In step 3, we made use of the Permissions helper library. Permissions has a method
called askAsync, which takes one parameter defining what type of permissions
your app would like to request from the user. Permissions also has constants for
each type of permission you can request from the user. These permission types
include LOCATION, NOTIFICATIONS (which we'll use later in this chapter), CAMERA,
AUDIO_RECORDING, CONTACTS, CAMERA_ROLL, and CALENDAR. Since we need the location in this
recipe, we passed in the constant Permissions.LOCATION. Once the askAsync return
promise resolves, the return object will have a status property and an expiration
property. If the user has allowed the requested permission, status will be set to
the 'granted'string. If granted, we will fire off our getLocation method.
In step 4, we defined the function that gets the location from the device's GPS.
We call the getCurrentPositionAsync method of the Location component. This method
will return an object with a coords property and a timestamp property. The coords
property gives us access to the latitude and longitude, as well as the altitude,
accuracy (radius of uncertainty for the location, measured in meters),
altitudeAccuracy (accuracy of the altitude value, in meters (iOS only)), heading, and
speed. Once received, we save the location to state so that the render function will
be called, and our map will be rendered.
In step 5, we defined the renderMap method to render the map. First, we check
whether there is a location, and if there is, we render the MapView element. This
element only requires us to define the value for one property: initialRegion. This
property takes an object with four properties: latitude, longitude, latitudeDelta, and
longitudeDelta. We set the latitude and longitude equal to those in the state object,
and provide initial values for latitudeDelta and longitudeDelta. These last two
properties dictate the initial zoom level that the map should be rendered at; the
larger this number is, the more zoomed out the map will be. I suggest
experimenting with these two values to see how they affect the rendered map.
In step 6, we added the marker to the map by adding a MapView.Marker element as a
child of the MapView element. We defined the coordinates by passing the info saved
on state (state.location.coords) to the coords property, and set a title and description
for the marker's popup when tapped. We were also able to easily define a custom
pin by inlining our custom image with a require statement in the image property.
There's more...
As mentioned previously, you can read the docs for the react-native-maps project
to learn more about the features of this excellent library (https://github.com/react-c
ommunity/react-native-maps). For instance, you can easily customize the appearance
of your map by using Google Maps Styling Wizard (https://mapstyle.withgoogle.com
/) to generate a mapStyle JSON object, then pass that object to the MapView
component's customMapStyle property. Or, you could add geometric shapes to your
map with the Polygon and Circle components.
Once you're ready to deploy your app, there are a few follow-up steps that you
will need to take to take to ensure the map works properly on Android. You can
read the details on how deploying to a standalone Android app with
a MapView component works in the Expo documentation at https://docs.expo.io/versi
ons/latest/sdk/map-view#deploying-to-a-standalone-app-on-android.
Creating an audio player
Audio players are another common interface built into many applications.
Whether your app needs to play audio files stored locally on the device or stream
audio from a remote location, Expo's Audio component comes to the rescue.
In this recipe, we'll be building a full-fledged basic audio player, with
play/pause, next track, and previous track functionality. For simplicity, we'll be
hardcoding the information for the tracks we'll be using, but in a real-world
scenario, you'll likely be working with similar objects to what we're defining: an
object with a track title, album name, artist name, and a URL to a remote audio
file. I've chosen three random live tracks from the Internet Archive's Live Music
Archive (https://archive.org/details/etree).
Getting ready
We'll need to create a new app for this recipe. Let's call it audio-player.
How to do it...
1. Let's start by opening up App.js and adding the dependencies we'll need:
import React, { Component } from 'react';
import { Audio } from 'expo';
import { Feather } from '@expo/vector-icons';
import {
StyleSheet,
Text,
TouchableOpacity,
View,
Dimensions
} from 'react-native';
2. An audio player needs audio to play. We'll create a playlist array to hold the
audio tracks. Each track is represented by an object with a title, artist, album,
and uri:
const playlist = [
{
title: 'People Watching',
artist: 'Keller Williams',
album: 'Keller Williams Live at The Westcott Theater on 2012-09-22',
uri: 'https://ia800308.us.archive.org/7/items/kwilliams2012-09-22.at853.flac16/kwilliams2012-09-22at853.t16.mp3'
},
{
title: 'Hunted By A Freak',
artist: 'Mogwai',
album: 'Mogwai Live at Ancienne Belgique on 2017-10-20',
uri: 'https://ia601509.us.archive.org/17/items/mogwai2017-10-20.brussels.fm/Mogwai2017-10-20Brussels-07.mp3'
},
{
title: 'Nervous Tic Motion of the Head to the Left',
artist: 'Andrew Bird',
album: 'Andrew Bird Live at Rio Theater on 2011-01-28',
uri: 'https://ia800503.us.archive.org/8/items/andrewbird2011-01-28.early.dr7.flac16/andrewbird2011-01-28.early.t07.mp3'
}
];
3. Next, we'll define our App class and initial state object with four properties:
isPlaying for defining whether the player is playing or paused
playbackInstance to hold the Audio instance
volume and currentTrackIndex for the currently playing track
isBuffering to display a Buffering... message while the track is
buffering at the beginning of playback
As shown in following code:
export default class App extends Component {
state = {
isPlaying: false,
playbackInstance: null,
volume: 1.0,
currentTrackIndex: 0,
isBuffering: false,
}
// Defined in following steps
}
4. Let's define the componentDidMount life cycle hook next. We'll use this method
to configure the Audio component via the setAudioModeAsync method, passing in
an options object with a few recommended settings. These will be discussed
more in the How it works... section at the end of the recipe. After this, we'll
load the audio with loadAudio, defined in the next step:
async componentDidMount() {
await Audio.setAudioModeAsync({
allowsRecordingIOS: false,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
playsInSilentModeIOS: true,
shouldDuckAndroid: true,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
});
this.loadAudio();
}
5. The loadAudio function will handle loading the audio for our player. First,
we'll create a new instance of Audio.Sound. We'll then call the
setOnPlaybackStatusUpdate method on our new Audio instance, passing in a
handler that will be called whenever the state of playback within the
instance has changed. Finally, we call loadAsync on the instance, passing it a
source from the playlist array, as well as a status object with the volume and
a shouldPlay property set to the isPlaying value of state. The third parameter
dictates whether we want to wait for the file to finish downloading before it
is played, so we pass in false:
async loadAudio() {
const playbackInstance = new Audio.Sound();
const source = {
uri: playlist[this.state.currentTrackIndex].uri
}
const status = {
shouldPlay: this.state.isPlaying,
volume: this.state.volume,
};
playbackInstance.setOnPlaybackStatusUpdate(this.onPlaybackStatusUpdate);
await playbackInstance.loadAsync(source, status, false);
this.setState({
playbackInstance
});
}
6. We still need to define the callback for handling status updates. All we need
to do in this function is set the value of isBuffering on state to the isBuffering
value on the status parameter that was passed in from the
setOnPlaybackStatusUpdate function call:
onPlaybackStatusUpdate = (status) => {
this.setState({
isBuffering: status.isBuffering
});
}
7. Our app now knows how to load an audio file from the playlist array and
update state with the current buffering status of the loaded audio file, which
we'll use later in the render function to display a message to the user. All
that's left is to add the behavior for the player itself. First, we'll handle the
play/pause state. The handlePlayPause method checks the value
of this.state.isPlaying to determine whether the track should be played or
paused, and calls the associated method on the playbackInstance accordingly.
Finally, we need to update the value of isPlaying for state:
handlePlayPause = async () => {
const { isPlaying, playbackInstance } = this.state;
isPlaying ? await playbackInstance.pauseAsync() : await playbackInstance.playAsync();
this.setState({
isPlaying: !isPlaying
});
}
8. Next, let's define the function for handling skipping to the previous track.
First, we'll clear the current track from the playbackInstance by
calling unloadAsync. We'll update the currentTrackIndex value of state to either
one less than the current value, or 0 if we're at the beginning of the playlist
array. Then, we'll call this.loadAudio to load the proper track:
handlePreviousTrack = async () => {
let { playbackInstance, currentTrackIndex } = this.state;
if (playbackInstance) {
await playbackInstance.unloadAsync();
currentTrackIndex < playlist.length - 1 ? currentTrackIndex
+= 1 : currentTrackIndex = 0;
this.setState({
currentTrackIndex
});
this.loadAudio();
}
}
9. Not surprisingly, handleNextTrack is the same as the preceding function, but
this time we'll either add 1 to the current index, or set the index to 0 if we're
at the end of the playlist array:
handleNextTrack = async () => {
let { playbackInstance, currentTrackIndex } = this.state;
if (playbackInstance) {
await playbackInstance.unloadAsync();
currentTrackIndex < playlist.length - 1 ? currentTrackIndex +=
1 : currentTrackIndex = 0;
this.setState({
currentTrackIndex
});
this.loadAudio();
}
}
10. It's time to define our render function. We will need three basic pieces in our
UI: a 'Buffering...' message when the track is playing but still buffering, a
section for displaying information for the current track, and a section to
hold the player's controls. The 'Buffering...' message will only display if
both this.state.isBuffering and this.state.isPlaying are true. The song info is
rendered via the renderSongInfo method, which we'll define in step 12:
render() {
return (
<View style={styles.container}>
<Text style={[styles.largeText, styles.buffer]}>
{this.state.isBuffering && this.state.isPlaying ?
'Buffering...' : null}
</Text>
{this.renderSongInfo()}
<View style={styles.controls}>
// Defined in next step.
</View>
</View>
);
}
11. The player controls are made up of three TouchableOpacity button elements,
each with a corresponding icon from the Feather icon library. You can find
more information on using icons in Chapter 3, Implementing Complex User
Interfaces – Part I. We'll determine whether to display the Play icon or the
Pause icon depending on the value of this.state.isPlaying:
<View style={styles.controls}>
<TouchableOpacity
style={styles.control}
onPress={this.handlePreviousTrack}
>
<Feather name="skip-back" size={32} color="#fff"/>
</TouchableOpacity>
<TouchableOpacity
style={styles.control}
onPress={this.handlePlayPause}
>
{this.state.isPlaying ?
<Feather name="pause" size={32} color="#fff"/> :
<Feather name="play" size={32} color="#fff"/>
}
</TouchableOpacity>
<TouchableOpacity
style={styles.control}
onPress={this.handleNextTrack}
>
<Feather name="skip-forward" size={32} color="#fff"/>
</TouchableOpacity>
</View>
12. The renderSongInfo method returns basic JSX for displaying the metadata
associated with the track currently playing:
renderSongInfo() {
const { playbackInstance, currentTrackIndex } = this.state;
return playbackInstance ?
<View style={styles.trackInfo}>
<Text style={[styles.trackInfoText, styles.largeText]}>
{playlist[currentTrackIndex].title}
</Text>
<Text style={[styles.trackInfoText, styles.smallText]}>
{playlist[currentTrackIndex].artist}
</Text>
<Text style={[styles.trackInfoText, styles.smallText]}>
{playlist[currentTrackIndex].album}
</Text>
</View>
: null;
}
13. All that's left to add are the styles. The styles defined here are well-covered
ground by now, and don't go beyond centering, colors, font size, and adding
padding and margins:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#191A1A',
alignItems: 'center',
justifyContent: 'center',
},
trackInfo: {
padding: 40,
backgroundColor: '#191A1A',
},
buffer: {
color: '#fff'
},
trackInfoText: {
textAlign: 'center',
flexWrap: 'wrap',
color: '#fff'
},
largeText: {
fontSize: 22
},
smallText: {
fontSize: 16
},
control: {
margin: 20
},
controls: {
flexDirection: 'row'
}
});
14. You can now check out your app in the simulator, and you should have a
fully working audio player! Note that audio playback in the Android
emulator may be too slow for the playback to work properly, and may
sound very choppy. Open the app on a real Android device to hear the track
playing properly:
How it works...
In step 4, we initialized options on the Audio component once the app had finished
loading via the componentDidMount method. The Audio component's setAudioModeAsync
method takes an option object as its only parameter.
Let's review the options we used in this recipe:
interruptionModeIOS and interruptionModeAndroid set how the audio in your app
should interact with the audio from other applications on the device. We
used the Audio component's INTERRUPTION_MODE_IOS_DO_NOT_MIX
and INTERRUPTION_MODE_ANDROID_DO_NOT_MIX enums, respectively, to declare that our
app's audio should interrupt any other applications playing audio.
playsInSilentModeIOS is a Boolean that determines whether your app should
play audio when the device is in silent mode.
shouldDuckAndroid is a Boolean that determines whether your app's audio
should lower in volume (duck) when audio from another app interrupts
your app. While this setting defaults to true, I've added it to the recipe so
that you're aware that it's an option.
In step 5, we defined the loadAudio method, which performs the heavy lifting in
this recipe. First, we created a new instance of the Audio.Sound class and saved it to
the playbackInstance variable for later use. Next, we set the source and status
variables that will be passed into the loadAsync function on the playbackInstance for
actually loading the audio file. In the source object, we set the uri property to the
corresponding uri property on the object in the playlist array at the index stored
in this.state.currentTrackIndex. In the status object, we set the volume to the volume
value saved on state, and set shouldPlay, a Boolean that determines whether the
audio should be playing initially, to this.state.isPlaying. And, since we want to
stream the remote MP3 file instead of waiting for the entire file to download, we
pass false the third, downloadFirst, parameter.
Before calling the loadAsync method, we first
called setOnPlaybackStatusUpdate of playbackInstance, which takes a callback function
that should be called when the state of playbackInstance has changed. We defined
that handler in step 6. The handler simply saves the isBuffering value from the
callback's status parameter to the isBuffering property of state, which will fire a
rerender, updating the 'Buffering...' message in the UI accordingly.
In step 7, we defined the handlePlayPause function for toggling play and pause
functionality in the app. If there's a track playing, this.state.isPlaying will be true,
so we'll call the pauseAsync function on the playbackInstance otherwise, we'll call
playAsync to start playing the audio again. Once we've played or paused, we
update the value of isPlaying on state.
In step 8 and step 9, we created the functions that handle skipping to the next
and previous tracks. Each of these functions increases or decreases the value of
this.state.currentTrackIndex as appropriate, so that by the time this.loadAudio is
called at the bottom of each function, it will load the track associated with the
object in the playlist array at the new index.
There's more...
The features of our current app are more basic than you'll find in most audio
players, but all the tools you need for building a feature-rich audio player are at
your disposal. For instance, you could display the current track time in the UI by
tapping into the positionMillis property on the status parameter in the
setOnPlaybackStatusUpdate callback. Or, you could use a React Native Slider
component to allow the user to adjust the volume or playback rate. Expo's Audio
component provides all the building blocks for a great audio player app.
Creating an image carousel
There are all kinds of applications that make use of image carousels. Any time
there's a collection of images that you'd like your user to be able to peruse, a
carousel is likely among the most effective UI patterns for accomplishing the
task.
There are a number of packages in the React Native community for handling the
creation of carousels, but in my experience none are more stable or more
versatile than react-native-snap-carousel (https://github.com/archriss/react-native-sn
ap-carousel). This package provides a great API for customizing the look and
behavior of your carousel, and supports Expo app development without the need
for detaching. You can easily change how slides appear as they slide in and out
of the carousel frame via the Carousel component's layout property, and as of
version 3.6, you can even create custom interpolations!
While you are not limited to only displaying images with this package, we'll be
building a carousel that just displays images along with a caption to keep the
recipe simple. We'll be using the excellent license-free photo site unsplash.com to
get random images for displaying in our carousel via the Unsplash Source
project hosted at source.unsplash.com. Unsplash Source allows you to easily request
random images from Unsplash without needing to access the official API. You
can visit the Unsplash Source site for more information on how it works.
Getting ready
We'll need to create a new app for this recipe. Let's call this app carousel.
How to do it...
1. We'll start by opening App.js and importing dependencies:
import React, { Component } from 'react';
import {
SafeAreaView,
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
Picker,
Dimensions,
} from 'react-native';
import Carousel from 'react-native-snap-carousel';
2. Next, let's define the App class and the initial state object. The state has three
properties: a Boolean for whether we're currently displaying the carousel or
not, a layoutType property for setting the layout style of our carousel, and an
array of imageSearchTerms we'll use later to get images from Unsplash Source.
Feel free to change the imageSearchTerms array to your heart's content:
export default class App extends React.Component {
state = {
showCarousel: false,
layoutType: 'default',
imageSearchTerms: [
'Books',
'Code',
'Nature',
'Cats',
]
}
// Defined in following steps
}
3. Let's define the render method next. We'll just check the value of
this.state.showCorousel and either show the carousel or the controls
accordingly:
render() {
return (
<SafeAreaView style={styles.container}>
{this.state.showCarousel ?
this.renderCarousel() :
this.renderControls()
}
</SafeAreaView>
);
}
4. Next, let's create the renderControls function. This will be the layout the user
sees when they first open the app, and consists of a React Native Picker for
selecting a layout type to use in the carousel and a button for opening the
carousel. The Picker has three options available: default, tinder, and stack:
renderControls = () => {
return(
<View style={styles.container}>
<Picker
selectedValue={this.state.layoutType}
style={styles.picker}
onValueChange={this.updateLayoutType}
>
<Picker.Item label="Default" value="default" />
<Picker.Item label="Tinder" value="tinder" />
<Picker.Item label="Stack" value="stack" />
</Picker>
<TouchableOpacity
onPress={this.toggleCarousel}
style={styles.openButton}
>
<Text style={styles.openButtonText}>Open Carousel</Text>
</TouchableOpacity>
</View>
)
}
5. Let's define the toggleCarousel function. This function simply sets the value
of showCarousel on state to its opposite. By defining a toggle function, we can
use the same function to both open and close the carousel:
toggleCarousel = () => {
this.setState({
showCarousel: !this.state.showCarousel
});
}
6. Similarly, the updateLayoutType method just updates the layoutType on state to
the layoutType value passed into it from the Picker component:
updateLayoutType = (layoutType) => {
this.setState({
layoutType
});
}
7. The renderCarousel function returns the markup for the carousel. It's made up
of a button for closing the carousel and the Carousel component itself. This
component takes a layout property, as set by the Picker. It also has a
data property, which takes the data that should be looped over for each
carousel slide, and a renderItem callback that handles the rendering of each
individual slide:
renderCarousel = () => {
return(
<View style={styles.carouselContainer}>
<View style={styles.closeButtonContainer}>
<TouchableOpacity
onPress={this.toggleCarousel}
style={styles.button}
>
<Text style={styles.label}>x</Text>
</TouchableOpacity>
</View>
<Carousel
layout={this.state.layoutType}
data={this.state.imageSearchTerms}
renderItem={this.renderItem}
sliderWidth={350}
itemWidth={350}
>
</Carousel>
</View>
);
}
8. We still need the function that handles the rendering of each slide. This
function receives one object parameter containing the next item in the array
passed to the data property. We'll return an Image component that uses the
item parameter value to get a random item from Unsplash Source that's
350x350 in size. We'll also add a Text element to display the type of image
being displayed:
renderItem = ({item}) => {
return (
<View style={styles.slide}>
<Image
style={styles.image}
source={{ uri: `https://source.unsplash.com/350x350/?
${item}`}}
/>
<Text style={styles.label}>{item}</Text>
</View>
);
}
9. The last thing we'll need is some styles to lay out our UI. The container
styles apply to the main wrapping SafeAreaView element, so we set
justifyContent to 'space-evenly' so that the Picker and TouchableOpacity
components fill up the screen. To display the close button in the top-right
corner of the screen, we'll apply flexDirection: 'row and justifyContent: 'flex-
end' to the wrapping element. The rest of the styles are just dimensions,
colors, padding, margins, and font size:
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'space-evenly',
},
carouselContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#474747'
},
closeButtonContainer: {
width: 350,
flexDirection: 'row',
justifyContent: 'flex-end'
},
slide: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
image: {
width:350,
height: 350,
},
label: {
fontSize: 30,
padding: 40,
color: '#fff',
backgroundColor: '#474747'
},
openButton: {
padding: 10,
backgroundColor: '#000'
},
openButtonText: {
fontSize: 20,
padding: 20,
color: '#fff',
},
closeButton: {
padding: 10
},
picker: {
height: 150,
width: 100,
backgroundColor: '#fff'
}
});
10. We've completed our carousel app. It probably won't win any design
awards, but it's a working carousel app with smooth, native-feeling
behavior:
How it works...
In step 4, we defined the renderControls function, which renders the UI when the
app is first launched. This is the first recipe in which we've used the Picker
component. It's a part of the core React Native library and provides the drop-
down type selector used to select options in many applications. The selectedValue
property is the value tied to whichever item is currently selected in the picker.
By setting it to this.state.layoutType, we'll default the selection to the 'default'
layout, and keep the values synced when a different Picker item is selected. Each
item in the picker is represented by a Picker.Item component. Its label property
defines the display text for the item, and the value property represents the string
value for the item. Since we provided the onValueChange property with the
updateLayoutType function, it will be called whenever a new item is selected, which
in turn will update this.state.layoutType accordingly.
In step 7, we defined the JSX for the carousel. The carousel's data and renderItem
properties are required, and work together to render each slide in the carousel.
When the carousel is instantiated, the array passed into the data property will be
looped over, and the renderItem callback function will be called for each item in
the area, with that item passed into the renderItem as a parameter. We also set the
sliderWidth and itemWidth properties, which are required for horizontal carousels.
In step 8, we defined the renderItem function that gets called for each entry in the
array passed into data. We set the source of the returned Image component to an
Unsplash source URL, which will return a random image of the type requested.
There's more...
There are a few things we could do to improve this recipe. We could make use of
the Image.prefetch() method to download the first image before opening the
carousel, so that the image is ready right away, or add an input to allow the user
to select their own image search terms.
The react-native-snap-carousel package provides a great way to build a
multimedia carousel for a React Native app. There are a number of features we
didn't have the time to cover here, including parallax images and custom
pagination. For the adventurous developer, the package provides a way to create
custom interpolations, allowing you to make your own layouts beyond the three
built-in layouts.
Adding push notifications to your app
Push notifications are a great way to provide a constant feedback loop between
the app and the user by continually providing app-specific data that's relevant to
the user. Messaging applications send notifications when new messages arrive.
Reminder applications display a notification to remind the user of a task at a
specific time or location. A podcast app might use notifications to inform the
user that a new episode has been published. A shopping app could use
notifications to alert the user to check out a limited-time deal.
Push notifications are a proven way to increase user interaction and retention. If
your app makes use of time-sensitive or event-based data, push notifications
could be a valuable asset. In this recipe, we'll be using Expo's push notification
implementation, which simplifies some of the setup that would be required with
a vanilla React Native project. If the needs of your app demand a non-Expo
project, I would recommend considering the react-native-push-notification
package at https://github.com/zo0r/react-native-push-notification.
In this recipe, we'll be making a very simplistic messaging app with push
notifications. We'll request proper permissions, then register a push notification
token to an Express server we'll be building. We'll also render a TextInput for the
user to enter a message into. When the Send button is pressed, the message will
be sent to our server, and the server will send a push notification via Expo's push
notification server, with the message from the app, to all devices that have
registered a token with our Express server.
Thanks to Expo's built-in push notification service, the complicated work of
creating a notification for each native device is offloaded to an Expo hosted
backend. The Express server we build in this recipe will just pass off JSON
objects for each push notification to the Expo backend, and the rest is taken care
of. The following diagram from the Expo docs (https://docs.expo.io/versions/latest
/guides/push-notifications) illustrates the life cycle of a push notification:
Image source: https://docs.expo.io/versions/latest/guides/push-notifications/
While implementing push notifications using Expo is less setup work than it
would otherwise be, the requirements of the technology still mean we will need
to run a server for handling registrations and sending notifications, which means
this recipe will be a little longer than most. Let's get started!
Getting ready
One of the first things we'll need to do in this app is request permission from the
device to use push notifications. Unfortunately, push notification permissions do
not work properly in emulators, so a real device will be needed to test this app.
We'll also need to be able to access the push notification server from an address
outside of the localhost. In a real-world setup, the push notification server would
already have a public URL, but in a development environment, the easiest
solution is to create a tunnel that exposes the development push notification
server to the internet. We'll be using the ngrok tool for this purpose, since it is a
mature, robust, and incredibly easy-to-use solution. You can read more about the
software at https://ngrok.com.
First, install ngrok globally via npm using the following command:
npm i -g ngrok
Once it's installed, you can create a tunnel from the internet to a port on your
local machine by executing ngrok with the https parameter:
ngrok https [port-to-expose]
We'll use this command later in the recipe to expose the development server.
Let's create a new app for this recipe. We'll call it push-notifications. We're going
to need three extra npm packages for this recipe: express for the push notification
server, esm for using ES6 syntax support on the server, and expo-server-sdk for
processing push notifications. Install them with yarn:
yarn add express esm expo-server-sdk
Alternatively, install them using npm:
npm install express esm expo-server-sdk --save
How to do it...
1. Let's start with building the App. We'll start that by adding the dependencies
we need to App.js:
import React from 'react';
import {
StyleSheet,
Text,
View,
TextInput,
Modal,
TouchableOpacity
} from 'react-native';
import { Permissions, Notifications } from 'expo';
2. We're going to declare two constants for the API endpoints on our server,
but the url will be generated by ngrok when we run the server later in the
recipe, so we'll update the value of these constants at that point:
const PUSH_REGISTRATION_ENDPOINT = 'http://generated-ngrok-url/token';
const MESSAGE_ENPOINT = 'http://generated-ngrok-url/message';
3. Let's create the App component and initialize the state object. We'll need
a notification property to hold notifications received by the Notifications
listener, which we will define in a later step:
export default class App extends React.Component {
state = {
notification: null,
messageText: ''
}
// Defined in following steps
}
4. Let's define the method that will handle registering the push notification
token to the server. We'll ask for notification permission from the user via
the askAsync method on the Permissions component. If permission is granted,
get the token from the device from the getExpoPushTokenAsync method of
the Notifications component:
registerForPushNotificationsAsync = async () => {
const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
if (status !== 'granted') {
return;
}
let token = await Notifications.getExpoPushTokenAsync();
// Defined in following steps
}
5. Once we have the appropriate token, we'll send it over to the push
notification server for registration. We will then make a POST request
to PUSH_REGISTRATION_ENDPOINT, sending a token object and user object in the
request body. I've hardcoded the values in the user object, but in a real app
this would be the metadata you've stored for the current user:
registerForPushNotificationsAsync = async () => {
// Defined in above step
return fetch(PUSH_REGISTRATION_ENDPOINT, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: {
value: token,
},
user: {
username: 'warly',
name: 'Dan Ward'
},
}),
});
// Defined in next step
}
6. After the token is registered, we'll set up an event listener to listen to any
notifications that occur while the app is open and foregrounded. In certain
cases, we will need to manually handle displaying the information from an
incoming push notification. Check the How it works... section at the end of
this recipe for more on why this is necessary and how it can be leveraged.
We'll define the handler in the next step:
registerForPushNotificationsAsync = async () => {
// Defined in above steps
this.notificationSubscription =
Notifications.addListener(this.handleNotification);
}
7. Whenever a new notification is received, the handleNotification method will
be run. We'll just store the new notification passed to this callback on the
state object for later use in the render function:
handleNotification = (notification) => {
this.setState({ notification });
}
8. We want our app to ask for permission to use push notifications, and to
register the push notification token when the app launches. We'll utilize the
componentDidMount life cycle hook to run our registerForPushNotificationsAsync
method:
componentDidMount() {
this.registerForPushNotificationsAsync();
}
9. The UI will be very minimal to keep the recipe simple. It's made up of
a TextInput for the message text, a Send button for sending the message, and
a View for displaying any notifications heard by the notification listener:
render() {
return (
<View style={styles.container}>
<TextInput
value={this.state.messageText}
onChangeText={this.handleChangeText}
style={styles.textInput}
/>
<TouchableOpacity
style={styles.button}
onPress={this.sendMessage}
>
<Text style={styles.buttonText}>Send</Text>
</TouchableOpacity>
{this.state.notification ?
this.renderNotification()
: null}
</View>
);
}
10. The TextInput component defined in the previous step is missing the method
it needs for its onChangeText property. Let's create that method next. It just
saves the text input by the user to this.state.messageText so it can be used by
the value property and elsewhere:
handleChangeText = (text) => {
this.setState({ messageText: text });
}
11. The TouchableOpacity component's onPress property calls the sendMessage method
to send the message text when the user presses the button. In this function,
we'll just take the message text and POST it to the MESSAGE_ENDPOINT on our push
notification server. The server will handle things from there. Once the
message is sent, we'll clear the messageText property on state:
sendMessage = async () => {
fetch(MESSAGE_ENPOINT, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: this.state.messageText,
}),
});
this.setState({ messageText: '' });
}
12. The last piece we need for the App is the styles. These styles are
straightforward, and should all look quite familiar by now:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#474747',
alignItems: 'center',
justifyContent: 'center',
},
textInput: {
height: 50,
width: 300,
borderColor: '#f6f6f6',
borderWidth: 1,
backgroundColor: '#fff',
padding: 10
},
button: {
padding: 10
},
buttonText: {
fontSize: 18,
color: '#fff'
},
label: {
fontSize: 18
}
});
13. With the React Native app portion out of the way, let's move on to the
server portion. First, we'll create a new server folder in the root of the
project with an index.js file inside of it. Let's start by importing express to run
the server and expo-server-sdk to handle the registration and sending of push
notifications. We'll create an Express server app and store it in the app const,
and a new instance of the Expo server SDK in the expo const. We'll also
add a savedPushTokens array for storing any tokens that are registered with the
React Native app, and a PORT_NUMBER const for the port we want to run the
server on:
import express from 'express';
import Expo from 'expo-server-sdk';
const app = express();
const expo = new Expo();
let savedPushTokens = [];
const PORT_NUMBER = 3000;
14. Our server will need to expose two endpoints (one for registering tokens,
and one for accepting messages from the React Native app), so we'll create
two functions that will be executed when these routes are hit. We'll define
the saveToken function first. It just takes a token, checks whether it's stored in
the savedPushTokens array, and pushes it to the array if it isn't there already:
const saveToken = (token) => {
if (savedPushTokens.indexOf(token === -1)) {
savedPushTokens.push(token);
}
}
15. The other function our server needs is a handler for sending push
notifications when a message is received from the React Native app. We'll
loop over all of the tokens that have been saved to the savedPushTokens array
and create a message object for each token. Each message object has a title
of Message received!, which will display in bold on the push notification, and
the message text as the body of the notification:
const handlePushTokens = (message) => {
let notifications = [];
for (let pushToken of savedPushTokens) {
if (!Expo.isExpoPushToken(pushToken)) {
console.error(`Push token ${pushToken} is not a valid Expo push token`);
continue;
}
notifications.push({
to: pushToken,
sound: 'default',
title: 'Message received!',
body: message,
data: { message }
})
}
// Defined in following step
}
16. Once we have an array of messages, we can send them to Expo's server,
which in turn will send the push notification to all registered devices. We'll
send the messages array via the expo
server's chunkPushNotifications and sendPushNotificationsAsync methods, and
console.log the success receipts, or an error, as appropriate to the server
console. There's more on how this works in the How it works... section at
the end of this recipe:
const handlePushTokens = (message) => {
// Defined in previous step
let chunks = expo.chunkPushNotifications(notifications);
(async () => {
for (let chunk of chunks) {
try {
let receipts = await expo.sendPushNotificationsAsync(chunk);
console.log(receipts);
} catch (error) {
console.error(error);
}
}
})();
}
17. Now that we have the functions defined for handling push notifications and
messages, let's expose those functions by creating API endpoints. If you're
not familiar with Express, it's a powerful and easy-to-use framework for
running a web server in Node. You can quickly get up to speed on the
basics of routing with the basic routing docs at https://expressjs.com/en/starter
/basic-routing.html.
We'll be working with JSON data, so the first step will be applying the
JSON parser middleware with a call to express.json():
app.use(express.json());
18. Even though we won't really be using the root path (/) of the server, it's
good practice to define one. We'll just respond with a message that the
server is running:
app.get('/', (req, res) => {
res.send('Push Notification Server Running');
});
19. First, let's implement the endpoint for saving a push notification token.
When a POST request is sent to the /token endpoint, we'll pass the token value
to the saveToken function and return a response stating that the token was
received:
app.post('/token', (req, res) => {
saveToken(req.body.token.value);
console.log(`Received push token, ${req.body.token.value}`);
res.send(`Received push token, ${req.body.token.value}`);
});
20. Likewise, the /message endpoint will take the message from the request body
and pass it to the handlePushTokens function for processing. Then, we'll send
back a response that the message was received:
app.post('/message', (req, res) => {
handlePushTokens(req.body.message);
console.log(`Received message, ${req.body.message}`);
res.send(`Received message, ${req.body.message}`);
});
21. The last piece to the server is the call to Express's listen method on the
server instance, which will start the server:
app.listen(PORT_NUMBER, () => {
console.log('Server Online on Port ${PORT_NUMBER}');
});
22. We're going to need a way to start the server, so we'll add a custom script to
the package.json file called serve. Open the package.json file and update it to
have a scripts object with a new serve script. With this added, we can run the
server with yarn via the yarn run serve command or with npm via the following
command: npm run serve. The package.json file should look something like this:
{
"main": "node_modules/expo/AppEntry.js",
"private": true,
"dependencies": {
"esm": "^3.0.28",
"expo": "^27.0.1",
"expo-server-sdk": "^2.3.3",
"express": "^4.16.3",
"react": "16.3.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-27.0.0.tar.gz"
},
"scripts": {
"serve": "node -r esm server/index.js"
}
}
23. We've got all the code in place, let's use it! As mentioned previously, push
notification permissions do not work properly on the emulator, so a real
device will be needed to test the push notification functionality. First, we'll
fire up our newly created server by running the following commands:
yarn run serve
npm run serve
You should be greeted by the Server Online message we defined in
the listen method call in step 21:
24. Next, we'll need to run ngrok to expose our server to the internet. Open a
new Terminal window and create an ngrok tunnel with the following
command:
ngrok http 3000
You should see the ngrok interface in the Terminal. This displays the
URLs generated by ngrok. In this case, ngrok is forwarding my server
located at http://localhost:3000 to the URL http://ddf558bd.ngrok.io. Let's
copy that URL:
25. You can test that the server is running and accessible from the internet by
visiting the generated URL in a browser. Navigating directly to this URL
behaves exactly the same as navigating to http://localhost:3000, which means
the GET endpoint we defined in previous step should run. That function
returns the Push Notification Server Running string, and should display in
your browser:
26. Now that we've confirmed that the server is running, let's update the React
Native app to use the correct server URL. In step 2, we added to constants
to hold our API endpoints, but we didn't have the correct URL yet. Let's
update these URLs to reflect the tunnel URL generated by ngrok:
const PUSH_REGISTRATION_ENDPOINT = 'http://ddf558bd.ngrok.io/token';
const MESSAGE_ENPOINT = 'http://ddf558bd.ngrok.io/message';
27. As mentioned previously, you'll need to run this app on a real device for the
permissions request to work correctly. As soon as you open the app, you
should be prompted by the device, asking if you'd like to allow the app to
send notifications:
28. As soon as Allow is selected, the push notification token will be sent to the
server's /token endpoint to be saved. This should also print the associated
console.log statement in the server Terminal with the saved token. In this
case, my iPhone's push token is the string
ExponentPushToken[g5sIEbOm2yFdzn5VdSSy9n]:
29. At this point, if you have a second Android or iOS device, go ahead and
open the React Native app on that device as well. If not, don't worry.
There's another easy way to test that our push notification functionality is
working without using a second device.
30. Use the React Native app to send a message. If you've got a second device
that has registered a token with the server, it should receive a push
notification corresponding to the newly sent message. You should also see
two new instances of console.log in the server: one that displays the received
message, and another that displays the receipts array received back from the
Expo servers. Each receipt object in the array will have a status property
with the value 'ok' if the operation was successful:
31. If you don't have a second device to test on, you can use Expo's push
notification tool, hosted at https://expo.io/dashboard/notifications. Just copy the
push token from the server Terminal and paste it into the input labeled EXPO
PUSH TOKEN (from your app). To emulate a message sent from our React
Native app, set MESSAGE TITLE to Message received!, MESSAGE BODY
to the message text you'd like to send, and check the Play Sound checkbox.
If you like, you can also emulate the data object by providing a JSON object
with a key of "message" and a value of your message text,such as { "message":
"This is a test message." }. The received message should then look something
like this screenshot:
How it works...
The recipe we built here is a little contrived, but the core concepts needed to
request permissions, register tokens, accept app data, and send push notifications
in response to app data are all there.
In step 4, we defined the first part of the registerForPushNotificationsAsync function.
We began by asking the user for their permission to send them notifications from
our app via the Permissions.askAsync method, passing in the enum for the push
notifications permission, Permissions.NOTIFICATIONS. We then saved the status
property from the resolved return object, which will have the value 'granted' if the
user granted permission. If we don't get permission, we return right away;
otherwise, we get the token from Expo's Notifications component by
calling getExpoPushTokenAsync. This function returns a token string, which will be in
the following format:
ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]
In step 5, we defined the POST call to the server's registration endpoint (/token).
This function sends the token in the request body, which is then saved on the
server using the saveToken function defined in step 14.
In step 6, we created an event listener that will listen for any new incoming push
notifications. This is done by calling Notifications.addListener and passing in a
callback function to be executed every time a new notification is received. On
iOS devices, the system is designed to only produce a push notification if the
app sending the push notification isn't open and foregrounded. That means if you
try to send your user a push notification while they're currently using your app,
they will never receive it.
To overcome this issue, Expo suggests manually displaying the push notification
data from within your app. This Notifications.addListener method was created to
fulfill this need. When a push notification is received, the callback passed
to addListener will be executed and will receive the new notification object as a
parameter. In step 7, we saved this notification to state so that the UI would be
re-rendered accordingly. We only displayed the message text in a Text component
in this recipe, but you could also use a modal for a more notification-like
presentation.
In step 11, we created the sendMessage function, which posts the message text
stored on state to the server's /message endpoint. This will execute
the handlePushToken server function defined in step 15.
In step 13, we started working on the server, which utilizes Express and the Expo
server SDK. A new server is created with express by calling express() directly, as
a local const, usually named app by convention. We were able to create a new
Expo server SDK instance with new Expo(), storing it in the expo const. We later
used the Expo server SDK to send the push notification using expo, define routes
using app in steps 17 to step 20, and initiate the server by calling app.listen() in
step 22.
In step 14, we defined the saveToken function, which will be executed when
the /token endpoint is used by the React Native app to register a token. This
function saves the incoming token to the savedPushTokens array, to be used later
when a message arrives from a user. In a real app, this is where you would likely
want to save the tokens to a persistent database of some kind, such as SQL,
MongoDB, or Firebase Database.
In step 15, we started defining the handlePushTokens function, which runs when the
React Native app uses the /message endpoint. The function loops over the
savedPushTokens array for processing. Each token is checked for validity using the
Expo server SDK's isExpoPushToken method, which takes in a token and returns true
if the token is valid. If it's invalid, we log an error to the server console. If it's
valid, we push a new notification object onto the local notifications array for
batch processing in the next step. Each notification object requires a to property
with the value set to a valid Expo push token. All other properties are optional.
The optional properties we set were as follows:
Sound: Can be default to play the default notification sound or null for no
sound
Title: The title of the push notification, usually displayed in bold
Body: The body of the push notification
Data: A custom data JSON object
In step 16, we used the Expo server SDK's chunkPushNotifications instance
method to create an array of data chunks optimized for sending to Expo's push
notification server. We then looped over the chunks, and sent each chunk to
Expo's push notification server via the expo.sendPushNotificationsAsync method. It
returned a promise that resolved to an array of receipts for each push
notification. If the process is successful, there will be a { status: 'ok' } object for
each notification in the array.
This endpoint's behavior is simpler than a real server would probably be,
because most message applications would have a more complicated way of
handling a message. At the very least, there would likely be a list of recipients
that would dictate which registered devices would in turn receive a particular
push notification. The logic was intentionally kept simple to portray the basic
flow.
In step 18, we defined the first accessible route on our server, the root (/) path.
Express provides the get and post helper methods for easily making API
endpoints for GET and POST requests respectively. The callback function receives a
request object and response object as parameters. All server URLs need to
respond to the request; otherwise, the request would time out. The response is
sent via the send method on the response object. This route doesn't process any
data, so we just returned the string indicating that our server is running.
In step 19 and step 20, we defined POST endpoints for /token and /message, which
will execute saveToken and handlePushTokens respectively. We also added console.log
statements to each, to log the token and the message to the server Terminal for
ease of development.
In step 21, we defined the listen method on our Express server, which starts the
server. The first parameter is the port number to listen for requests on, and the
second parameter is a callback function, usually used to console.log a message to
the server Terminal that the server has been started.
In step 22, we added a custom script to the package.json file of our project. Any
command that can be run in the Terminal can be made a custom npm script by
adding a scripts key to the package.json file set to an object whose keys are the
name of the custom script, and whose values are the command that should be
executed when that custom script is run. In this recipe, we defined a custom
scripted named serve that runs the node -r esm server/index.js command. This
command runs our server file (server/index.js) with Node, using the esm npm
package we installed at the beginning of this recipe. Custom scripts can be
executed with npm:
npm run [custom-script-name]
They can also be executed using yarn:
yarn run [custom-script-name]
There's more...
Push notifications can be complicated, but thankfully Expo simplifies the
process in a number of ways. There's great documentation on Expo's push
notification service, which covers the specifics of notification timing, Expo
server SDKs in other languages, and how to implement notifications over
HTTP/2. I encourage you to read more at https://docs.expo.io/versions/latest/guides
/push-notifications.
Implementing browser-based
authentication
In the Logging in with Facebook recipe in Chapter 8, Working with Application
Logic and Data, we will cover using the Expo Facebook component to create a
login workflow for providing our app with the user's basic Facebook
account information. Expo also provides a Google component, which provides
similar functionality for getting a user's Google account information. But what
do we do if we want to create a login workflow that uses account information
from a different site? In this case, Expo provides the AuthSession component.
AuthSession is built on Expo's WebBrowser component, which we've already used in Ch
apter 4, Implementing Complex User Interfaces – Part II. The typical login
workflow consists of four steps:
1. The user initiates the login process
2. The web browser opens to the login page
3. The authentication provider provides a redirect on successful login
4. The React Native app handles the redirect
In this app, we'll be using the Spotify API to get Spotify account information for
our app via user login. Head over to https://beta.developer.spotify.com/dashboard/appl
ications to create a new Spotify dev account (if you don't already have one) and a
new app. The app can be named whatever you like. Once the app is created with
Spotify, you'll see a client ID string displayed in the information for your app.
We'll need this ID when building the React Native app.
Getting ready
We will need a new app for this recipe. Let's name the app browser-based-auth.
The redirect URI also needs to be whitelisted in the Spotify app we created
previously. The redirect should be in the form
of https://auth.expo.io/@YOUR_EXPO_USERNAME/YOUR_APP_SLUG. Since my Expo username is
warlyware, and since this React Native app we're building is named browser-based-
auth, my redirect URI is https://auth.expo.io/@warlyware/browser-based-auth. Be sure to
add this to the Redirect URIs list in the settings of the Spotify app.
How to do it...
1. We'll start by opening App.js and importing the dependencies we will be
using:
import React, { Component } from 'react';
import { TouchableOpacity, StyleSheet, Text, View } from 'react-native';
import { AuthSession } from 'expo';
import { FontAwesome } from '@expo/vector-icons';
2. Let's also declare the Client-ID as a constant to be used later. Copy the Client-
ID for the Spotify app we created previously so that we can save it in the
CLIENT_ID const:
const CLIENT_ID = Your-Spotify-App-Client-ID;
3. Let's create the App class and the initial state. The userInfo property will hold
the user information we receive back from the Spotify API, and didError is a
Boolean for tracking whether an error occurred during login:
export default class App extends React.Component {
state = {
userInfo: null,
didError: false
};
// Defined in following steps
}
4. Next, let's define the method that logs the user in to Spotify. The AuthSession
component's getRedirectUrl method provides the redirect URL needed for
returning to the React Native app after login, which is the same redirect
URI we saved in the Spotify app in the Getting ready section of this recipe.
We'll then use the redirect URL in the login request, which we'll launch
with the AuthSession.startAsync method, passing in an options object with the
authUrl property set to the Spotify endpoint for authorizing user data with an
app. There's more information on this URL in the How it works... section at
the end of this recipe:
handleSpotifyLogin = async () => {
let redirectUrl = AuthSession.getRedirectUrl();
let results = await AuthSession.startAsync({
authUrl:
`https://accounts.spotify.com/authorize?client_id=${CLIENT_ID}
&redirect_uri=${encodeURIComponent(redirectUrl)}
&scope=user-read-email&response_type=token`
});
// Defined in next step
};
5. We saved the results of hitting the Spotify endpoint for user authentication
in the localresults variable. If the type property on the results object returns
anything other than 'success', then an error occurred, so we'll update the
didError property of state accordingly. Otherwise, we'll hit the /me endpoint
with the access token we received from authorization to get the user's info,
which we'll save to this.state.userInfo:
handleSpotifyLogin = async () => {
if (results.type !== 'success') {
this.setState({ didError: true });
} else {
const userInfo = await axios.get(`https://api.spotify.com/v1/me`, {
headers: {
"Authorization": `Bearer ${results.params.access_token}`
}
});
this.setState({ userInfo: userInfo.data });
}
};
6. Now that the auth related methods are defined, let's create the render
function. We'll use the FontAwesome Expo icon library to display the Spotify
logo, add a button to allow the user to log in, and add methods for rendering
either an error or the user info, depending on the value of this.state.didError.
We'll also disable the login button once there's data saved on the userInfo
property of state:
render() {
return (
<View style={styles.container}>
<FontAwesome
name="spotify"
color="#2FD566"
size={128}
/>
<TouchableOpacity
style={styles.button}
onPress={this.handleSpotifyLogin}
disabled={this.state.userInfo ? true : false}
>
<Text style={styles.buttonText}>
Login with Spotify
</Text>
</TouchableOpacity>
{this.state.didError ?
this.displayError() :
this.displayResults()
}
</View>
);
}
7. Next, let's define the JSX for handling errors. The template just displays a
generic error message to indicate that the user should try again:
displayError = () => {
return (
<View style={styles.userInfo}>
<Text style={styles.errorText}>
There was an error, please try again.
</Text>
</View>
);
}
8. The displayResults function will be a View component that displays the user's
image, username, and email address if there is userInfo saved to state,
otherwise it will prompt the user to log in:
displayResults = () => {
{ return this.state.userInfo ? (
<View style={styles.userInfo}>
<Image
style={styles.profileImage}
source={ {'uri': this.state.userInfo.images[0].url} }
/>
<View>
<Text style={styles.userInfoText}>
Username:
</Text>
<Text style={styles.userInfoText}>
{this.state.userInfo.id}
</Text>
<Text style={styles.userInfoText}>
Email:
</Text>
<Text style={styles.userInfoText}>
{this.state.userInfo.email}
</Text>
</View>
</View>
) : (
<View style={styles.userInfo}>
<Text style={styles.userInfoText}>
Login to Spotify to see user data.
</Text>
</View>
)}
}
9. The styles for this recipe are quite simple. It uses a column flex layout,
applies the Spotify color scheme of black and green, and adds font sizes and
margins:
const styles = StyleSheet.create({
container: {
flexDirection: 'column',
backgroundColor: '#000',
flex: 1,
alignItems: 'center',
justifyContent: 'space-evenly',
},
button: {
backgroundColor: '#2FD566',
padding: 20
},
buttonText: {
color: '#000',
fontSize: 20
},
userInfo: {
height: 250,
width: 200,
alignItems: 'center',
},
userInfoText: {
color: '#fff',
fontSize: 18
},
errorText: {
color: '#fff',
fontSize: 18
},
profileImage: {
height: 64,
width: 64,
marginBottom: 32
}
});
10. Now, if we look at the app, we should be able to log in to Spotify, and see
the associated image, username, and email address for the account used to
log in:
How it works...
In step 4, we created the method for handling the Spotify login process. The
AuthSession.startAsync method just needed an authUrl, which was provided by the
Spotify Developers documentation. The four pieces required are the Client-ID, the
redirect URI for handling the response from Spotify, a scope parameter indicating
the scope of user information the app is requesting, and a response_type parameter
of token. We only need basic information from the user, so we requested a scope
type of user-read-email. For information on all the scopes available, check the
documentation at https://beta.developer.spotify.com/documentation/general/guides/scopes
/.
In step 5, we completed the Spotify login handler. If the login was not
successful, we updated didError on state accordingly. If it was successful, we used
that response to access the Spotify API endpoint for getting user data (https://api
.spotify.com/v1/me). We defined the Authorization header of the GET request
with Bearer ${results.params.access_token} to validate the request, as per Spotify's
documentation. On the success of this request, we stored the returned user data
in the userInfo state object, which re-rendered the UI and displayed the user's
information.
For a deeper dive into Spotify's auth process, you can find the guide at https://bet
a.developer.spotify.com/documentation/general/guides/authorization-guide/.
See also
Expo Permissions docs: https://docs.expo.io/versions/latest/sdk/permissions
Expo MapView docs: https://docs.expo.io/versions/latest/sdk/map-view
Airbnb's React Native Maps package: https://github.com/react-community/react-n
ative-maps
Expo Audio docs: https://docs.expo.io/versions/latest/sdk/audio
React Native Image Prefetch docs: https://facebook.github.io/react-native/docs/
image.html#prefetch
React Native Snap Carousel Custom Interpolations docs: https://github.com/a
rchriss/react-native-snap-carousel/blob/master/doc/CUSTOM_INTERPOLATIONS.md
Expo Push Notifications docs: https://docs.expo.io/versions/latest/guides/push-n
otifications
Express Basic Routing guide: https://expressjs.com/en/starter/basic-routing.html
esm package: https://github.com/standard-things/esm
Expo server SDK for Node: https://github.com/expo/exponent-server-sdk-node
ngrok package: https://github.com/inconshreveable/ngrok
Adding Basic Animations to Your
App
In this chapter, we will cover the following recipes:
Creating simple animations
Running multiple animations
Creating animated notifications
Expanding and collapsing containers
Creating a button with a loading animation
Introduction
In order to provide a good user experience, we'll likely want to add some
animations to direct the user's attention, to highlight specific actions, or just to
add a distinctive touch to our app.
There's an initiative in progress to move all the processing from JavaScript to the
native side. At the time of writing (React Native Version 0.55), we can choose to
use the native driver to run all these calculations in the native world.
Unfortunately, this cannot be used with all animations, particularly those related
to layout, such as flexbox properties. Read more about caveats when using
native animation in the documentation at http://facebook.github.io/react-native/docs/
animations#caveats.
All of the recipes in this chapter use the JavaScript implementation. The React
Native team has promised to use the same API when moving all of the
processing to the native side, so we don't need to worry about breaking changes
to the existing API.
Creating simple animations
In this recipe, we will learn the basics of animations. We will use an image to
create a simple linear movement from the right to the left of the screen.
Getting ready
In order to go through this recipe, we need to create an empty app. Let's call it
simple-animation.
We are going to use a PNG image of a cloud for this recipe. You can find the
image in the recipe's repository hosted on GitHub at https://github.com/warlyware/re
act-native-cookbook/tree/master/chapter-6/simple-animation/assets/images. Place the
image in the /assets/images folder for use in the app.
How to do it...
1. Let's begin by opening App.js and importing the dependencies for the App
class. The Animated class will be responsible for creating the values for the
animation. It provides a few components that are ready to be animated, and
it also provides several methods and helpers to run smooth animations.
The Easing class provides several helper methods for both calculating
movements (such as linear and quadratic) and predefined animations (such
as bounce, ease, and elastic).
We are going to use the Dimensions class to get the current device size so that
we know where to place the element in the initialization of the animation:
import React, { Component } from 'react';
import {
Animated,
Easing,
Dimensions,
StyleSheet,
View,
} from 'react-native';
2. We'll also initialize some constants that we are going to need in our app. In
this case, we are going to get the device dimensions, set the size of the
image, and require our image that will be animated:
const { width, height } = Dimensions.get('window');
const cloudImage = require('./assets/images/cloud.png');
const imageHeight = 200;
const imageWidth = 300;
3. Now, let's create the App component. We are going to use two methods from
the component's life cycle system. If you are not familiar with this concept,
please review the related React docs (http://reactjs.cn/react/docs/component-spe
cs.html). This page also has a really nice tutorial on how life cycle hooks
work. You can also read more about this in Chapter 4, Implementing Complex
User Interfaces: Part II, in the Detecting orientation changes recipe:
export default class App extends Component {
componentWillMount() {
// Defined on step 4
}
componentDidMount() {
// Defined on step 7
}
startAnimation () {
// Defined on step 5
}
render() {
// Defined on step 6
}
}
const styles = StyleSheet.create({
// Defined on step 8
});
4. In order to create an animation, we need to define a standard value to drive
the animation. Animated.Value is a class that handles the animation values for
each frame over time. The first thing we need to do is to create an instance
of this class when the component is created. In this case, we are using the
componentWillMount method, but we can also use the constructor or even the
default values of a property:
componentWillMount() {
this.animatedValue = new Animated.Value();
}
5. Once we have created the animated value, we can define the animation. We
are also creating a loop by passing the start method of Animated.timing an
arrow function that executes this startAnimation function again. Now, when
the image reaches the end of the animation, we will start the same
animation again to create an infinitely looping animation:
startAnimation() {
this.animatedValue.setValue(width);
Animated.timing(
this.animatedValue,
{
toValue: -imageWidth,
duration: 6000,
easing: Easing.linear,
useNativeDriver: true,
}
).start(() => this.startAnimation());
}
6. We have our animation in place, but we are currently only calculating the
values for each frame over time, not doing anything with those values. The
next step is to render the image on the screen and set the property on the
styles that we want to animate. In this case, we want to move the element
on the x-axis; therefore, we should update the left property:
render() {
return (
<View style={styles.background}>
<Animated.Image
style={[
styles.image,
{ left: this.animatedValue },
]}
source={cloudImage}
/>
</View>
);
}
7. If we refresh the simulator, we will see the image on the screen, but it's not
being animated yet. In order to fix this, we need to call the startAnimation
method. We will start the animation once the component is fully rendered,
using the componentDidMount lifecycle hook:
componentDidMount() {
this.startAnimation();
}
8. If we run the app again, we will see how the image is moving at the top of
the screen, t what we want! As a final step, just what we want! As a final
step, let's add some basic styles to the app:
const styles = StyleSheet.create({
background: {
flex: 1,
backgroundColor: 'cyan',
},
image: {
height: imageHeight,
position: 'absolute',
top: height / 3,
width: imageWidth,
},
});
The output is as shown in following screenshot:
How it works...
In step 5, we set the animation values. The first line resets the initial value every
time we call this method. For this example, the initial value will be the width of
the device, which will move the image to the right-hand side of the screen,
where we want to start our animation.
Then, we use the Animated.timing function to create an animation based on time
and take two parameters. For the first parameter, we pass in animatedValue, which
we created in the componentWillMount lifecycle hook in step 4. The second parameter
is an object with configurations for the animation. In this case, we are going to
set the end value to minus the width of the image, which will place the image on
the left-hand side of the screen. We complete the animation there.
With the entire configuration in place, the Animated class will calculate all the
frames required in the 6 seconds allotted to perform a linear animation from
right to left (via the duration property being set to 6000 milliseconds).
We have another helper provided by React Native that can be paired with
Animated, called Easing. In this case, we are using the linear property of the Easing
helper class. Easing provides other common easing methods, such
as elastic and bounce. Take a look at the Easing class documentation and try setting
different values for the easing property to see how each works. You can find the
documentation at https://facebook.github.io/react-native/docs/easing.html.
Once the animation is configured correctly, we need to run it. We do this by
calling the start method. This method receives an optional callback function
parameter that will be executed when the animation is completed. In this case,
we are running the same startAnimation function recursively. This will create an
infinite loop, which is what we want to achieve.
In step 6, we are rendering the image. If we want to animate an image, we
should always use the Animate.Image component. Internally, this component will
handle the values of the animation and will set each value for every frame on the
native component. This avoids running the render method in the JavaScript layer
on every frame, allowing for smoother animations.
Along with the Image, we can also animate the View, Text, and ScrollView
components. There's support for all four of these components out of the box, but
we could also create a new component and add support for animations via
Animated.createAnimatedComponent(). All four of these components are able to handle
style changes. All we have to do is pass animatedValue to the property that we want
to animate, in this case the left property, but we could use any of the available
styles on each component.
Running multiple animations
In this recipe, we will learn how to use the same animation values in several
elements. This way, we can reuse the same values, along with interpolation, to
get different values for the remaining elements.
This animation will be similar to the previous recipe. This time, we will have
two clouds: one will be smaller with slower movement, the other larger and
faster moving. At the center of the screen, we will have a static airplane. We
won't add any animation to the airplane, but the moving clouds will make it
appear as though the plane is moving.
Getting ready
Let's start this recipe by creating an empty app called multiple-animations.
We are going to use three different images: two clouds and an airplane. You can
download the images from the recipe's repository, hosted on GitHub at https://git
hub.com/warlyware/react-native-cookbook/tree/master/chapter-6/multiple-animations/assets/i
mages. Make sure to place the images in the /assets/images folder.
How to do it...
1. Let's start by opening App.js and adding our imports:
import React, { Component } from 'react';
import {
View,
Animated,
Image,
Easing,
Dimensions,
StyleSheet,
} from 'react-native';
2. Additionally, we need to define some constants and require the images that
we are going to use for the animations. Note that we're using the same
cloud image as cloudImage1 and cloudImage2, but we will treat them as separate
entities in this recipe:
const { width, height } = Dimensions.get('window');
const cloudImage1 = require('./assets/images/cloud.png');
const cloudImage2 = require('./assets/images/cloud.png');
const planeImage = require('./assets/images/plane.gif');
const cloudHeight = 100;
const cloudWidth = 150;
const planeHeight = 60;
const planeWidth = 100;
3. In the next step, we are going to create the animatedValue instance when the
component gets created, then we will start the animation when the
component is fully rendered. We are creating an animation that runs in an
infinite loop. The initial value will be 1 and the final value will be 0. If you
are not clear about this code, make sure to read the first recipe in this
chapter:
export default class App extends Component {
componentWillMount() {
this.animatedValue = new Animated.Value();
}
componentDidMount() {
this.startAnimation();
}
startAnimation () {
this.animatedValue.setValue(1);
Animated.timing(
this.animatedValue,
{
toValue: 0,
duration: 6000,
easing: Easing.linear,
}
).start(() => this.startAnimation());
}
render() {
// Defined in a later step
}
}
const styles = StyleSheet.create({
// Defined in a later step
});
4. The render method in this recipe is going to be quite different from the last.
In this recipe, we are going to animate two images using the same
animatedValue. The animated value will return values from 1 to 0; however, we
want to move the clouds from right to left, so we need to set the left value
on each element.
In order to set the correct values, we need to interpolate animatedValue. For
the smaller cloud, we will set the initial left value to the width of the
device, but for the bigger cloud, we will set the initial left value far away
from the right-hand edge of the device. This will make the movement
distance bigger, and therefore it will move faster:
render() {
const left1 = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-cloudWidth, width],
});
const left2 = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-cloudWidth*5, width + cloudWidth*5],
});
// Defined in a later step
}
5. Once we have the correct left values, we need to define the elements we
want to animate. Here, we will set the interpolated value to the left styles
property:
render() {
// Defined in a later step
return (
<View style={styles.background}>
<Animated.Image
style={[
styles.cloud1,
{ left: left1 },
]}
source={cloudImage1}
/>
<Image
style={styles.plane}
source={planeImage}
/>
<Animated.Image
style={[
styles.cloud2,
{ left: left2 },
]}
source={cloudImage2}
/>
</View>
);
}
6. As for the last step, we need to define some styles, just to set the width and
height of each cloud as well as assign the top:
const styles = StyleSheet.create({
background: {
flex: 1,
backgroundColor: 'cyan',
},
cloud1: {
position: 'absolute',
width: cloudWidth,
height: cloudHeight,
top: height / 3 - cloudWidth / 2,
},
cloud2: {
position: 'absolute',
width: cloudWidth * 1.5,
height: cloudHeight * 1.5,
top: height/2,
},
plane: {
position: 'absolute',
height: planeHeight,
width: planeWidth,
top: height / 2 - planeHeight,
left: width / 2 - planeWidth,
}
});
7. If we refresh our app, we should see the animation:
How it works...
In step 4, we defined the interpolations to get the left value for each cloud. The
interpolate method receives an object with two required configurations,
inputRange and outputRange.
The inputRange configuration receives an array of values. These values should
always be ascending values; you could use negative values too, as long as the
values are ascending.
outputRange should match the number of values defined on inputRange. These are the
values that we need as a result of the interpolation.
For this recipe, inputRange goes from 0 to 1, which are the values of our
animatedValue. In outputRange, we defined the limits of the movement that we need.
Creating animated notifications
In this recipe, we will create a notification component from scratch. When
showing the notification, the component will slide in from the top of the screen.
After a few seconds, we will automatically hide it by sliding it out.
Getting ready
We are going to create an app. Let's call it notification-animation.
How to do it...
1. We'll start by working on the App component. First, let's import all the
required dependencies:
import React, { Component } from 'react';
import {
Text,
TouchableOpacity,
StyleSheet,
View,
SafeAreaView,
} from 'react-native';
import Notification from './Notification';
2. Once we have all the dependencies imported, we can define the App class. In
this case, we are going to initialize the state with a notify property equal to
false. We are going to use this property to show or hide the notification. By
default, the notification will not be shown onscreen. To make things simple,
we will define the message property in the state with the text we want to
display:
export default class App extends Component {
state = {
notify: false,
message: 'This is a notification!',
};
toggleNotification = () => {
// Defined on later step
}
render() {
// Defined on later step
}
}
const styles = StyleSheet.create({
// Defined on later step
});
3. Inside the render method, we need to show the notification only if the notify
property is true. We can achieve this by using an if statement:
render() {
const notify = this.state.notify
? <Notification
autoHide
message={this.state.message}
onClose={this.toggleNotification}
/>
: null;
// Defined on next step
}
4. In the previous step, we only defined the reference to the Notification
component, but we are not using it yet. Let's define a return with all of the
JSX needed for this app. To keep things simple, we are only going to define
a toolbar, some text, and a button to toggle the state of the notification when
pressed:
render() {
// Code from previous step
return (
<SafeAreaView>
<Text style={styles.toolbar}>Main toolbar</Text>
<View style={styles.content}>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing
elit,
sed do eiusmod tempor incididunt ut labore et
dolore magna.
</Text>
<TouchableOpacity
onPress={this.toggleNotification}
style={styles.btn}
>
<Text style={styles.text}>Show notification</Text>
</TouchableOpacity>
<Text>
Sed ut perspiciatis unde omnis iste natus error sit
accusantium doloremque laudantium.
</Text>
{notify}
</View>
</SafeAreaView>
);
}
5. We also need to define the method that toggles the notify property on the
state, which is very simple:
toggleNotification = () => {
this.setState({
notify: !this.state.notify,
});
}
6. We are almost done with this class. The only things left are the styles. In
this case, we will only add basic styles such as color, padding, fontSize,
backgroundColor, and margin, nothing really special:
const styles = StyleSheet.create({
toolbar: {
backgroundColor: '#8e44ad',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
padding: 10,
overflow: 'hidden',
},
btn: {
margin: 10,
backgroundColor: '#9b59b6',
borderRadius: 3,
padding: 10,
},
text: {
textAlign: 'center',
color: '#fff',
},
});
7. If we try to run the app, we will see an error that the ./Notification module
couldn't be resolved. Let's fix that by defining the Notification component.
Let's create a Notifications folder, with an index.js file inside of it. Then, we
can import our dependencies:
import React, { Componen } from 'react';
import {
Animated,
Easing,
StyleSheet,
Text,
} from 'react-native';
8. Once we have the dependencies imported, let's define the props and the
initial state of our new component. We are going to define something very
simple, just a property to receive the message to display, and two callback
functions to allow the running of some actions when the notification
appears on the screen and when it gets closed. We'll also add a property to
set the number of milliseconds to display the notification before it
autohides:
export default class Notification extends Component {
static defaultProps = {
delay: 5000,
onClose: () => {},
onOpen: () => {},
};
state = {
height: -1000,
};
}
9. It's finally time to work on the animation! We need to start the animation as
soon as the component gets rendered. If there's something not clear in the
following code, I recommend you take a look at the first and second recipes
in this chapter:
componentWillMount() {
this.animatedValue = new Animated.Value();
}
componentDidMount() {
this.startSlideIn();
}
getAnimation(value, autoHide) {
const { delay } = this.props;
return Animated.timing(
this.animatedValue,
{
toValue: value,
duration: 500,
easing: Easing.cubic,
delay: autoHide ? delay : 0,
}
);
}
10. So far, we've defined a method to get the animation. For the slide-in
movement, we need to calculate the values from 0 to 1. Once the animation
is complete, we need to run the onOpen callback. If the autoHide property is set
to true when the onOpen method is called, we will automatically run the slide-
out animation to remove the component:
startSlideIn () {
const { onOpen, autoHide } = this.props;
this.animatedValue.setValue(0);
this.getAnimation(1)
.start(() => {
onOpen();
if (autoHide){
this.startSlideOut();
}
});
}
11. Similar to the preceding step, we need a method for the slide-out
movement. Here, we need to calculate the values from 1 to 0. We are
sending the autoHide value as a parameter to the getAnimation method. This
will automatically delay the animation by the amount of milliseconds
defined by the delay property (in our case, 5 seconds). After the animation
has completed, we need to run the onClose callback function, which will
remove the component from the App class:
startSlideOut() {
const { autoHide, onClose } = this.props;
this.animatedValue.setValue(1);
this.getAnimation(0, autoHide)
.start(() => onClose());
}
12. Finally, let's add the render method. Here, we will get the message value
provided by props. We also need the height of the component to move the
component to the initial position of the animation; by default, it's -1000 but
we will set the correct value at runtime in the next steps. The animatedValue
goes from 0 to 1 or 1 to 0, depending on whether the notification is opening
or closing; therefore, we need to interpolate it to get the actual values. The
animation will go from minus the height of the component to 0; this will
result in a nice slide in/out animation:
render() {
const { message } = this.props;
const { height } = this.state;
const top = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-height, 0],
});
// Defined on next step
}
}
13. To keep things as simple as possible, we will return an Animated.View with
some text. Here, we are setting the top style with the interpolation result,
meaning we will animate the top style. As mentioned before, we need to
calculate the height of the component at runtime. In order to achieve that,
we need to use the onLayout property of the view. This function will be called
every time the layout updates and will send the new dimensions of this
component as a parameter:
render() {
// Code from previous step
return (
<Animated.View
onLayout={this.onLayoutChange}
style={[
styles.main,
{ top }
]}
>
<Text style={styles.text}>{message}</Text>
</Animated.View>
);
}
}
14. The onLayoutChange method will be very simple. We just need to get the new
height and update the state. This method receives an event. From this object,
we can grab useful information. For our purposes, we will access the data
at nativeEvent.layout in the event object. The layout object contains the
screen's width and height, and the x and y positions on the screen where
the Animated.View called this function:
onLayoutChange = (event) => {
const {layout: { height } } = event.nativeEvent;
this.setState({ height });
}
15. For the last step, we will add some styles to the notification component.
Since we want this component to animate on top of anything else, we need
to set the position to absolute, and set the left and right properties to 0. We'll
also add some color and padding:
const styles = StyleSheet.create({
main: {
backgroundColor: 'rgba(0, 0, 0, 0.7)',
padding: 10,
position: 'absolute',
left: 0,
right: 0,
},
text: {
color: '#fff',
},
});
16. The final app should look something like the following screenshot:
How it works...
In step 3, we defined the Notification component. This component receives three
parameters: a flag to automatically hide the component after a few seconds, the
message that we want to display, and a callback function that will be executed
when the notification gets closed.
When the onClose callback gets executed, we will toggle the notify property to
remove the Notification instance and clear the memory.
In step 4, we defined the JSX to render the components of our app. It's important
to render the Notification component after the others so that the component will
appear on top of all other components.
In step 6, we defined the state of our component. The defaultProps object sets the
default values for each property. These values will be applied if no value is
assigned to the given property.
We defined the default for each callback as an empty function. This way, we don't
have to check whether those props have a value before trying to execute them.
For the initial state, we defined the height property. The actual height value will be
calculated at runtime based on the content received in the message property. This
means we need to initially render the component far away from the original
position. Since there's a short delay when the layout is calculated, we don't want
to display the notification at all before it moves to the correct position.
In step 9, we created the animation. The getAnimation method receives two
parameters: the delay to be applied and the autoHide Boolean, which determines
whether the notification automatically closes. We used this method in step 10
and step 11.
In step 13, we returned the JSX for this component. The onLayout function is very
useful for getting the dimensions of the component when there are updates to the
layout. For example, if the device orientation changes, the dimensions will
change, in which case we would like to update the initial and final coordinates
for the animation.
There's more...
The current implementation works pretty well, but there's a performance
problem we should address. Currently, the onLayout method gets executed on
every frame of the animation, which means we are updating the state on every
frame, which leads to the component re-rendering on every frame! We should
avoid this, and only update it once to get the actual height.
To fix this, we could add a simple validation just to update the state if the current
value is different than the initial value. This will avoid updating the state on
every frame and we won't force the render over and over again:
onLayoutChange = (event) => {
const {layout: { height } } = event.nativeEvent;
if (this.state.height === -1000) {
this.setState({ height });
}
}
While this works for our purposes, we could also go further and make sure the
height also gets updated when the orientation changes. However, we'll stop here,
as this recipe is quite long already.
Expanding and collapsing containers
In this recipe, we will create a custom container element with a title and content.
When a user presses the title, the content will collapse or expand. This recipe
will allow us to explore the LayoutAnimation API.
Getting ready
Let's start by creating a new app. We'll call it collapsable-containers.
Once we have created the app, let's also create a Panel folder with an index.js file
in it for housing our Panel component.
How to do it...
1. Let's start by focusing on the Panel component. First, we need to import all
the dependencies that we are going to use for this class:
import React, { Component } from 'react';
import {
View,
LayoutAnimation,
StyleSheet,
Text,
TouchableOpacity,
} from 'react-native';
2. Once we have the dependencies, let's declare the defaultProps for initializing
this component. In this recipe, we only need to initialize the expanded
property to false:
export default class Panel extends Component {
static defaultProps = {
expanded: false
};
}
const styles = StyleSheet.create({
// Defined on later step
});
3. We are going to use the height property on the state object to expand or
collapse the container. The first time this component gets created, we need
to check the expanded property in order to set the correct initial height:
state = {
height: this.props.expanded ? null : 0,
};
4. Let's render the required JSX elements for this component. We need to get
the height value from state and set it to the content's style view. When
pressing the title element, we will execute the toggle method (defined later)
to change the height value of the state:
render() {
const { children, style, title } = this.props;
const { height } = this.state;
return (
<View style={[styles.main, style]}>
<TouchableOpacity onPress={this.toggle}>
<Text style={styles.title}>
{title}
</Text>
</TouchableOpacity>
<View style={{ height }}>
{children}
</View>
</View>
);
}
5. As mentioned before, the toggle method will be executed when the title
element is pressed. Here, we will toggle the height on the state and call the
animation we want to use when updating the styles on the next render
cycle:
toggle = () => {
LayoutAnimation.spring();
this.setState({
height: this.state.height === null ? 0 : null,
})
}
6. To complete this component, let's add some simple styles. We need to set
the overflow to hidden, otherwise the content will be shown when the
component is collapsed:
const styles = StyleSheet.create({
main: {
backgroundColor: '#fff',
borderRadius: 3,
overflow: 'hidden',
paddingLeft: 30,
paddingRight: 30,
},
title: {
fontWeight: 'bold',
paddingTop: 15,
paddingBottom: 15,
}
7. Once we have our Panel component defined, let's use it on the App class.
First, we need to require all the dependencies in App.js:
import React, { Component } from 'react';
import {
Text,
StyleSheet,
View,
SafeAreaView,
Platform,
UIManager
} from 'react-native';
import Panel from './Panel';
8. In the previous step, we imported the Panel component. We are going to
declare three instances of this class in the JSX:
export default class App extends Component {
render() {
return (
<SafeAreaView style={[styles.main]}>
<Text style={styles.toolbar}>Animated containers</Text>
<View style={styles.content}>
<Panel
title={'Container 1'}
style={styles.panel}
>
<Text style={styles.panelText}>
Temporibus autem quibusdam et aut officiis
debitis aut rerum necessitatibus saepe
eveniet ut et voluptates repudiandae sint et
molestiae non recusandae.
</Text>
</Panel>
<Panel
title={'Container 2'}
style={styles.panel}
>
<Text style={styles.panelText}>
Et harum quidem rerum facilis est et expedita
distinctio. Nam libero tempore,
cum soluta nobis est eligendi optio cumque.
</Text>
</Panel>
<Panel
expanded
title={'Container 3'}
style={styles.panel}
>
<Text style={styles.panelText}>
Nullam lobortis eu lorem ut vulputate.
</Text>
<Text style={styles.panelText}>
Donec id elementum orci. Donec fringilla lobortis
ipsum, vitae commodo urna.
</Text>
</Panel>
</View>
</SafeAreaView>
);
}
}
9. We are using the React Native LayoutAnimation API in this recipe. This API is
disabled on Android by default. In the current version of React Native,
before the App component mounts, we'll use the Platform helper with the
UIManager to enable this feature on Android devices:
componentWillMount() {
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
10. Finally, let's add some styles to the toolbar and the main container. We just
need some simple styles you're likely used to by now: padding, margin, and
color:
const styles = StyleSheet.create({
main: {
flex: 1,
},
toolbar: {
backgroundColor: '#3498db',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
padding: 10,
backgroundColor: '#ecf0f1',
flex: 1,
},
panel: {
marginBottom: 10,
},
panelText: {
paddingBottom: 15,
}
});
11. The final app should look similar to the following screenshots:
How it works...
In step 3, we set the initial height of the content. If the expanded property was set to
true, then we should show the content. By setting the height value to null, the
layout system will calculate the height based on the content; otherwise, we need
to set the value to 0, which will hide the content when the component is
collapsed.
In step 4, we defined all the JSX for the Panel component. There are a few
concepts in this step worth covering. First, the children property is passed in from
the props object, which will contain any elements defined between <Panel> and
</Panel> when this component is used in the App class. This is very helpful
because, by using this property, we are allowing this component to receive any
other components as children.
In this same step, we're also getting the height from the state object and setting it
as the style applied to the View with the collapsible content. This will update the
height, causing the component to correspondingly expand or collapse. We also
declared the onPress callback, which toggles the height on the state when
the title element is pressed.
In step 7, we defined the toggle method, which toggles the height value. Here, we
used the LayoutAnimation class. By calling the spring method, the layout system will
animate every change that happens to the layout on the next render. In this case,
we are only changing height, but we can change any other property we want, such
as opacity, position, or color.
The LayoutAnimation class contains a couple of predefined animations. In this
recipe, we used spring, but we could also use linear or easeInEaseOut, or you could
create your own using the configureNext method.
If we remove the LayoutAnimation, we won't see an animation; the component will
expand and collapse by jumping from 0 to total height. But by adding that single
line, we're able to easily add a nice, smooth animation. If you need more control
over the animation, you'll probably want to use the Animation API instead.
In step 9, we checked the OS property on the Platform helper, which returned the
'android' or 'ios' strings, depending on which device the app is running on. If the
app is running on Andriod, we use the UIManager helper's
setLayoutAnimationEnabledExperimental method to enable the LayoutAnimation API.
See also
LayoutAnimation API documentation at https://facebook.github.io/react-native/docs
/layoutanimation.html
A quick intro to React's props.children at https://codeburst.io/a-quick-intro-to-re
acts-props-children-cb3d2fce4891
Creating a button with a loading
animation
In this recipe, we'll continue working with the LayoutAnimation class. Here, we will
create a button, and when the user presses the button, we will show a loading
indicator and animate the styles.
Getting ready
To get started, we'll need to create an empty app. Let's call it button-loading-
animation.
Let's also create a Button folder with an index.js file in it for our Button component.
How to do it...
1. Let's start with the Button/index.js file. First, we'll import all the
dependencies for this component:
import React, { Component } from 'react';
import {
ActivityIndicator,
LayoutAnimation,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
2. We're going to use only four props for this component: a label, a loading
Boolean to toggle displaying either the loading indicator or the label inside
the button, a callback function to be executed when the button is pressed,
and custom styles. Here, we'll init the defaultProps for loading to false, and
the
handleButtonPress to an empty function:
export default class Button extends Component {
static defaultProps = {
loading: false,
onPress: () => {},
};
// Defined on later steps
}
3. We'll keep the render method of this component as simple as possible. We'll
render the label and the activity indicator based on the value of the loading
property:
render() {
const { loading, style } = this.props;
return (
<TouchableOpacity
style={[
styles.main,
style,
loading ? styles.loading : null,
]}
activeOpacity={0.6}
onPress={this.handleButtonPress}
>
<View>
{this.renderLabel()}
{this.renderActivityIndicator()}
</View>
</TouchableOpacity>
);
}
4. In order to render the label, we need to check whether the loading property is
false. If it is, then we return only a Text element with the label we received
from props:
renderLabel() {
const { label, loading } = this.props;
if(!loading) {
return (
<Text style={styles.label}>{label}</Text>
);
}
}
5. Likewise, the renderActivityIndicator indicator should only apply if the value
of the loading property is true. If so, we will return the ActivityIndicator
component. We'll use the props of ActivityIndicator to define a size of small
and a color of white (#fff):
renderActivityIndicator() {
if (this.props.loading) {
return (
<ActivityIndicator size="small" color="#fff" />
);
}
}
6. One method is still missing from our class: handleButtonPress. We need to
inform the parent of this component when the button has been pressed,
which can be done by calling the onPress callback passed to this component
via props. We'll also use the LayoutAnimation to queue an animation on the next
render:
handleButtonPress = () => {
const { loading, onPress } = this.props;
LayoutAnimation.easeInEaseOut();
onPress(!loading);
}
7. To complete this component, we need to add some styles. We'll define some
colors, rounded corners, alignment, padding, and so on. For the loading
styles, which will be applied when the loading indicator is displayed, we'll
update the padding to create a circle around the loading indicator:
const styles = StyleSheet.create({
main: {
backgroundColor: '#e67e22',
borderRadius: 20,
padding: 10,
paddingLeft: 50,
paddingRight: 50,
},
label: {
color: '#fff',
fontWeight: 'bold',
textAlign: 'center',
backgroundColor: 'transparent',
},
loading: {
padding: 10,
paddingLeft: 10,
paddingRight: 10,
},
});
8. We are done with the Button component. Now, lets's work on the App class.
Let's start by importing all the dependencies:
import React, { Component } from 'react';
import {
Text,
StyleSheet,
View,
SafeAreaView,
Platform,
UIManager
} from 'react-native';
import Button from './Button';
9. The App class is relatively simple. We will only need to define a loading
property on the state object, which will toggle the Button's animation. We'll
also render a toolbar and a Button:
export default class App extends Component {
state = {
loading: false,
};
// Defined on next step
handleButtonPress = (loading) => {
this.setState({ loading });
}
render() {
const { loading } = this.state;
return (
<SafeAreaView style={[styles.main, android]}>
<Text style={styles.toolbar}>Animated containers</Text>
<View style={styles.content}>
<Button
label="Login"
loading={loading}
onPress={this.handleButtonPress}
/>
</View>
</SafeAreaView>
);
}
}
10. As in the last recipe, we'll need to manually enable the LayoutAnimation API
on Android devices:
componentWillMount() {
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
11. Finally, we'll add some styles, just some colors, padding, and alignment for
centering the button on the screen:
const styles = StyleSheet.create({
main: {
flex: 1,
},
toolbar: {
backgroundColor: '#f39c12',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
padding: 10,
backgroundColor: '#ecf0f1',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
12. The final app should look similar to the following screenshot:
How it works...
In step 3, we added the render method for the Button component. Here, we
received the loading property and, based on that value, we applied the
corresponding styles to the TouchableOpacity button element. We also used two
methods: one for rendering the label and the other for rendering the activity
indicator.
In step 6, we executed the onPress callback. By default, we declared an empty
function, so we don't have to check whether the value is present or not.
The parent of this button should be responsible for updating the loading property
when the onPress callback is called. From this component, we are only
responsible for informing the parent when this button has been pressed.
The LayoutAnimation.eadeInEaseOut method only queues an animation for the next
render phase, which means the animation isn't executed right away. We are
responsible for changing the styles that we want to animate. If we don't change
any styles, then we won't see any animations.
The Button component doesn't know how the loading property gets updated. It
might be because of a fetch request, a timeout, or any other action. The parent
component is responsible for updating the loading property. Whenever any
changes happen, we apply the new styles to the button and a smooth animation
will occur.
In step 9, we defined the content of the App class. Here, we make use of our Button
component. When the button is pressed, the state of the loading property is
updated, which will cause the animation to run every time the button is pressed.
Conclusion
In this chapter, we've covered the fundamentals of animating your React Native
app. These recipes have been aimed at both providing useful practical code
solutions, and also establishing how to use the basic building blocks so that you
are better equipped to create animations that fit your app. Hopefully, by now,
you should be getting comfortable with the Animated and LayoutAnimation animation
helpers. In Chapter 7, Adding Advanced Animations to Your App, we will combine
the things we've learned here to build out more complex and interesting app-
centric UI animations.
Adding Advanced Animations to
Your App
In this chapter, we'll cover the following recipes:
Removing items from a list component
Creating a Facebook reactions widget
Displaying images in fullscreen
Introduction
In the previous chapter, we covered the basics of using the two main animation
helpers in React Native: Animated and LayoutAnimation. In this chapter, we'll take
these concepts further by building out more complicated recipes that exhibit
common native UX patterns.
Removing items from a list
component
In this recipe, we'll learn how to create list items in a ListView with an animated
sideways slide. If the user slides the item past a threshold, the item is removed.
This is a common pattern in many mobile apps with editable lists. We are also
going to see how to use PanResponder to handle drag events.
Getting ready
We need to create an empty app. For this recipe, we'll name it removing-list-items.
We also need to create a new ContactList folder and two files inside
it: index.js and ContactItem.js.
How to do it...
1. Let's start by importing the dependencies for the main App class, as follows:
import React from 'react';
import {
Text,
StyleSheet,
SafeAreaView,
} from 'react-native';
import ContactList from './ContactList';
2. This component will be simple. All we need to render is a toolbar and
the ContactList component that we imported in the previous step, as follows:
const App = () => (
<SafeAreaView style={styles.main}>
<Text style={styles.toolbar}>Contacts</Text>
<ContactList style={styles.content} />
</SafeAreaView>
);
const styles = StyleSheet.create({
main: {
flex: 1,
},
toolbar: {
backgroundColor: '#2c3e50',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
padding: 10,
flex: 1,
},
});
export default App;
3. This is all we need in order to start working on the actual list. Let's open the
file at ContactList/index.js and import all of the dependencies, as follows:
import React, { Component } from 'react';
import {
ListView,
ScrollView,
} from 'react-native';
import ContactItem from './ContactItem';
4. We then need to define some data. In a real-world app, we would fetch the
data from an API, but to keep things simple and focused only on the drag
functionality, let's just define the data in this same file:
const data = [
{ id: 1, name: 'Jon Snow' },
{ id: 2, name: 'Luke Skywalker' },
{ id: 3, name: 'Bilbo Baggins' },
{ id: 4, name: 'Bob Labla' },
{ id: 5, name: 'Mr. Magoo' },
];
5. The state for this component will only contain two properties: the data for
the list and a Boolean value that will be updated when the dragging starts or
ends. If you are not familiar with how ListView works, checkout the
Displaying a list of items recipe in Chapter 2, Creating a Simple React Native
App. Let's define the data as follows:
export default class ContactList extends Component {
ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
state = {
dataSource: this.ds.cloneWithRows(data),
swiping: false,
};
// Defined in later steps
}
6. The render method only needs to display the list. In
the renderScrollComponent property, we'll enable scrolling only when the user is
not swiping an item on the list. If the user is swiping, we want to disable
vertical scrolling, as follows:
render() {
const { dataSource, swiping } = this.state;
return (
<ListView
key={data}
enableEmptySections
dataSource={dataSource}
renderScrollComponent={
(props) => <ScrollView {...props} scrollEnabled={!swiping}/>
}
renderRow={this.renderItem}
/>
);
}
7. The renderItem method will return each item in the list. Here, we need to
send the contact information as a property, along with three callbacks:
renderItem = (contact) => (
<ContactItem
contact={contact}
onRemove={this.handleRemoveContact}
onDragEnd={this.handleToggleSwipe}
onDragStart={this.handleToggleSwipe}
/>
);
8. We need to toggle the value of the swiping property on the state object,
which will toggle whether vertical scroll on the list is locked or not:
handleToggleSwipe = () => {
this.setState({ swiping: !this.state.swiping });
}
9. When removing an item, we need to find the index of the given contact and
then remove it from the original list. After that, we need to
update datasource on the state to re-render the list with the resulting data:
handleRemoveContact = (contact) => {
const index = data.findIndex(
(item) => item.id === contact.id
);
data.splice(index, 1);
this.setState({
dataSource: this.ds.cloneWithRows(data),
});
}
10. We are done with the list, so now let's focus on the list items. Let's open
the ContactList/ContactItem.js file and import the dependencies we'll need:
import React, { Component } from 'react';
import {
Animated,
Easing,
PanResponder,
StyleSheet,
Text,
TouchableHighlight,
View,
} from 'react-native';
11. We need to define defaultProps for this component. The defaultProps object
will need an empty function for each of the four props being passed into it
from the parent ListView element. The onPress function will execute when the
item is pressed, the onRemove function will execute when the contact gets
removed, and two drag functions will listen for drag events. On state , we
only need to define an animated value to hold the x and y coordinates of the
dragging, as follows:
export default class ContactItem extends Component {
static defaultProps = {
onPress: () => {},
onRemove: () => {},
onDragEnd: () => {},
onDragStart: () => {},
};
state = {
pan: new Animated.ValueXY(),
};
12. When the component is created, we need to configure PanResponder. We will
do this in the componentWillMount life cycle hook. PanResponder is responsible for
handling gestures. It provides a simple API to capture the events generated
by the user's finger, as follows:
componentWillMount() {
this.panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: this.handleShouldDrag,
onPanResponderMove: Animated.event(
[null, { dx: this.state.pan.x }]
),
onPanResponderRelease: this.handleReleaseItem,
onPanResponderTerminate: this.handleReleaseItem,
});
}
13. Now let's define the actual functions that will get executed for each callback
defined in the previous step. We can start with the handleShouldDrag method,
as follows:
handleShouldDrag = (e, gesture) => {
const { dx } = gesture;
return Math.abs(dx) > 2;
}
14. handleReleaseItem is a little bit more complicated. We are going to split this
method into two steps. First, we need to figure out whether the current item
needs to be removed or not. In order to do that, we need to set a threshold.
If the user slides the element beyond our threshold, we'll remove the item,
as follows:
handleReleaseItem = (e, gesture) => {
const { onRemove, contact,onDragEnd } = this.props;
const move = this.rowWidth - Math.abs(gesture.dx);
let remove = false;
let config = { // Animation to origin position
toValue: { x: 0, y: 0 },
duration: 500,
};
if (move < this.threshold) {
remove = true;
if (gesture.dx > 0) {
config = { // Animation to the right
toValue: { x: this.rowWidth, y: 0 },
duration: 100,
};
} else {
config = { // Animation to the left
toValue: { x: -this.rowWidth, y: 0 },
duration: 100,
};
}
}
// Remainder in next step
}
15. Once we have the configurations for the animation, we are ready to move
the item! First, we'll execute the onDragEnd callback and, if the item should be
removed, we'll run the onRemove function, as follows:
handleReleaseItem = (e, gesture) => {
// Code from previous step
onDragEnd();
Animated.spring(
this.state.pan,
config,
).start(() => {
if (remove) {
onRemove(contact);
}
});
}
16. We have the full dragging system in place. Now we need to define
the render method. We just need to display the contact name within
the TouchableHighlight element, wrapped inside an Animated.View, as follows:
render() {
const { contact, onPress } = this.props;
return (
<View style={styles.row} onLayout={this.setThreshold}>
<Animated.View
style={[styles.pan, this.state.pan.getLayout()]}
{...this.panResponder.panHandlers}
>
<TouchableHighlight
style={styles.info}
onPress={() => onPress(contact)}
underlayColor="#ecf0f1"
>
<Text>{contact.name}</Text>
</TouchableHighlight>
</Animated.View>
</View>
);
}
17. We need one more method on this class, which is fired on layout change via
the View element's onLayout prop. setThreshold will get the
current width of row and set threshold. In this case, we're setting it to be a third
of the width of the screen. These values are required to decide whether to
remove the item or not, as follows:
setThreshold = (event) => {
const { layout: { width } } = event.nativeEvent;
this.threshold = width / 3;
this.rowWidth = width;
}
18. Finally, we'll add some styles to the rows, as follows:
const styles = StyleSheet.create({
row: {
backgroundColor: '#ecf0f1',
borderBottomWidth: 1,
borderColor: '#ecf0f1',
flexDirection: 'row',
},
pan: {
flex: 1,
},
info: {
backgroundColor: '#fff',
paddingBottom: 20,
paddingLeft: 10,
paddingTop: 20,
},
});
19. The final app should look something like this screenshot: